[
  {
    "path": ".gitignore",
    "content": "# IDEA files\n.idea\n*.iml\n\n# Maven files\ntarget\n\n# Docker compose files\ncompose/war\n\n# Npm modules\nnode_modules\n"
  },
  {
    "path": "AUTHORS",
    "content": "The following authors have created the source code of \"Selenium Grid Router\"\npublished and distributed by YANDEX LLC as the owner:\n\n* Alexander Andryashin  <aandryashin@yandex-team.ru>\n* Dmitry Baev           <charlie@yandex-team.ru>\n* Artem Eroshenko       <eroshenkoam@yandex-team.ru>\n* Innokenty Shuvalov    <innokenty@yandex-team.ru>\n* Ivan Krutov           <vania-pooh@yandex-team.ru>\n"
  },
  {
    "path": "LICENSE",
    "content": "(C) YANDEX LLC, 2015\n\nThe Source Code called \"Selenium Grid Router\" available at https://github.com/seleniumkit/gridrouter is subject\nto the terms of the Apache License 2.0 (hereinafter referred to as the \"License\").\nThe text of the License is the following:\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "# Selenium Grid Router\n\n**Selenium Grid Router** is a lightweight server that routes and proxies [Selenium Wedriver](http://www.seleniumhq.org/projects/webdriver/) requests to multiple Selenium hubs.\n\n## Golang Implementation\nThere is a smaller and faster Golang implementation of this server. See https://github.com/aerokube/ggr for more details.\n\n## What is this for\nIf you're frequently using Selenium for running your tests in browsers you may notice that a standard [Selenium Grid](https://github.com/SeleniumHQ/selenium/wiki/Grid2) installation has some faults that can prevent you from using it on large scale:\n* **Does not support high availability.** Selenium Grid consists of a single entry point **hub** server and multiple **node** processes. Users interact only with hub. That means that if for some reason hub goes down all nodes also become unavailable to user.\n* **Does not scale well.** Our experience shows that even when running on high-end hardware Selenium hub is able to handle correctly no more than 20-30 nodes. When more nodes are connected hub very often stops responding.\n* **Does not support authentication and authorization.** Standard Selenium grid hub makes all nodes available for everyone.\n\n## How it works\nThe basic idea is very simple:\n\n1. Define user names and their passwords in a plain text file\n2. Distribute a set of running Selenium hubs (aka \"hosts\") with nodes connected to each one over multiple datacenters\n3. For each defined user save hosts to a simple XML configuration file\n4. Start multiple instances of Grid Router in different datacenters and load-balance them\n5. Work with Grid Router like you do with a regular Selenium hub\n\n## Installation\n\nCurrently we maintain only Debian packages. To install on Ubuntu ensure that you have Java 8 installed:\n```\n# add-apt-repository ppa:webupd8team/java\n# apt-get update\n# apt-get install oracle-java8-installer\n```\nThen install Gridrouter itself:\n```\n# add-apt-repository ppa:yandex-qatools/gridrouter\n# apt-get update\n# apt-get install yandex-grid-router\n# service yandex-grid-router start\n```\nConfiguration files are located in `/etc/grid-router/` directory, XML quota files - by default in `/etc/grid-router/quota/`, log files reside in `/var/log/grid-router/`, binaries are installed to `/usr/share/grid-router`.\n\n## Configuration\nTwo types of configuration files exist:\n* A plain text file with users and passwords (users.properties)\n* An XML file with user quota definition (&lt;username&gt;.xml)\n\n### Users list (users.properties)\nA typical file looks like this:\n```\nalice:alicePassword, user\nbob:bobPassword, user\n```\nAs you can see passwords are **NOT** encrypted. This is because we consider quotas as a way to easily limit Selenium browsers consumption and not a restrictive tool.\n\n### User quota definition (&lt;username&gt;.xml)\nThis file has the following format:\n```xml\n<qa:browsers xmlns:qa=\"urn:config.gridrouter.qatools.ru\">\n    <browser name=\"firefox\" defaultVersion=\"33.0\">\n        <version number=\"33.0\">\n            <region name=\"us-west\">\n                <host name=\"my-firefox33-hub-1.example.com\" port=\"4444\" count=\"5\"/>\n            </region>\n            <region name=\"us-east\">\n                <host name=\"my-firefox33-hub-2.example.com\" port=\"4444\" count=\"5\"/>\n            </region>\n        </version>\n        <version number=\"37.0\">\n            <region name=\"us-west\">\n                <host name=\"my-firefox37-hub-1.example.com\" port=\"4444\" count=\"3\"/>\n                <host name=\"my-firefox37-hub-2.example.com\" port=\"4444\" count=\"4\"/>\n            </region>\n            <region name=\"us-east\">\n                <host name=\"my-firefox37-hub-3.example.com\" port=\"4444\" count=\"2\"/>\n            </region>\n        </version>\n    </browser>\n    <browser name=\"chrome\" defaultVersion=\"42.0\">\n        <version number=\"42.0\">\n            <region name=\"us-west\">\n                <host name=\"my-chrome42-hub-1.example.com\" port=\"4444\" count=\"10\"/>\n            </region>\n            <region name=\"us-east\">\n                <host name=\"my-chrome42-hub-2.example.com\" port=\"4444\" count=\"10\"/>\n            </region>\n        </version>\n    </browser>\n</qa:browsers>\n```\nWhat we basically do in this file - we enumerate hub hosts, ports and counts of browsers available on each hub. We also distribute hosts across regions, i.e. we place hosts from different datacenters in different **&lt;region&gt;** tags. The most important thing is to make sure that browser name and browser version have **exactly** the same value as respective Selenium hub does.\n\n### Authentication\nGrid router is using [BASIC HTTP authentication](https://en.wikipedia.org/wiki/Basic_access_authentication). That means that for the majority of test frameworks connection URL would be:\n```\nhttp://username:password@grid-router-host.example.com:4444/wd/hub\n```\nHowever some Javascript test frameworks have their own ways to specify connection URL, user name and password.\n\n### Hub selection logic\nWhen you request a browser by specifying its name and version **Grid Router** does the following:\n\n1. Searches for the browser in user quota XML and returns error if not found\n2. Randomly selects a host from all hosts and tries to obtain browser on that host. Our algorithm also considers browser counts specified in XML for each host so that hosts with more browsers get more connections.\n3. If browser was obtained - returns it to the user and proxies all requests in this session to the same host\n4. If not - selects a new host **from another region** and tries again. This guarantees that when one datacenter goes down in most of cases we'll obtain browser at worst after the second attempt.\n5. After trying all hosts returns error if no browser was obtained\n\n### Hub configuration recommendations\nOur experience shows that Grid Router works better with a big set of \"small\" hubs (having no more than 5 connected nodes) than with some \"big\" hubs. A good idea is to launch small virtual machines (with 1 or 2 virtual CPUs) containing one Selenium hub process 4-5 Selenium node processes that connect to **localhost**. This gives us the following profit:\n* Because we have more hubs the probability to successfully obtain browser is greater\n* If each virtual machine has only one browser version installed - it's simpler to increase overall count of available browsers\n* Hubs with small count of connected nodes perform better\n\n## Development\nWe're using [Docker](https://www.docker.com/) and [Ansible](http://www.ansible.com/) for integration tests so you need to install them on your Mac or Linux.\n\n### Install Boot2docker (dog-nail for Mac users)\n\n* Install Ansible: `brew install ansible`\n* Create an empty inventory file: `touch /usr/local/etc/ansible/hosts`\n* Adjust Python settings: `echo 'localhost ansible_python_interpreter=/usr/local/bin/python' >>  /usr/local/etc/ansible/hosts`\n* Instally Python from [official website](https://www.python.org/ftp/python/2.7.10/python-2.7.10-macosx10.6.pkg)\n* Install requests with pip: `pip install requests[security]`\n* Install docker-py: `pip install -Iv https://pypi.python.org/packages/source/d/docker-py/docker-py-1.1.0.tar.gz`\n* Run boot2docker: `boot2docker up`\n* Get Docker VM IP: `boot2docker ip`\n* Modify `/etc/hosts`: `<boot2docker_ip> boot2docker`\n* Add certificates information to console: `$(boot2docker shellinit)`\n* Export correct host name: `export DOCKER_HOST=tcp://boot2docker:2376`\n\n### Running service locally\n\n#### Start\n\n1. Build project: `mvn clean package`\n2. Start app: `ansible-playbook testing/start.yml`\n3. Check that container is running: `docker ps -a`\n\n#### Run integration tests\n\n```bash]\n$ ansible-playbook testing/test.yml\n```\n\n#### Stop\n\n```bash\n$ ansible-playbook testing/stop.yml\n```\n"
  },
  {
    "path": "ci/jenkins.groovy",
    "content": "def project = 'gridrouter';\ndef repo = 'seleniumkit/gridrouter'\n\ndef buildWarJob = mavenJob(\"${project}_build-war\")\ndef e2eTestsJob = job(\"${project}_e2e-tests\")\ndef sonarJob = mavenJob(\"${project}_sonar\")\ndef sonarIncrJob = mavenJob(\"${project}_sonar-incr\")\ndef deployJob = mavenJob(\"${project}_deploy\")\n\ndef pullRequestJob = multiJob(\"${project}_pull-reqest_flow\")\ndef snapshotJob = multiJob(\"${project}_snapshot_flow\")\ndef releaseJob = mavenJob(\"${project}_release_flow\")\n\nbuildWarJob.with {\n\n    label('maven')\n    scm {\n        git {\n            remote {\n                github(repo, 'https', 'github.com')\n                refspec('${GIT_REFSPEC}')\n            }\n            branch('${GIT_COMMIT}')\n            localBranch('master')\n        }\n    }\n    goals('clean package')\n\n    publishers {\n        archiveArtifacts('proxy/target/*.war')\n    }\n}\n\ne2eTestsJob.with {\n\n    label('e2e')\n    scm {\n        git {\n            remote {\n                github(repo, 'https', 'github.com')\n                refspec('${GIT_REFSPEC}')\n            }\n            branch('${GIT_COMMIT}')\n            localBranch('master')\n        }\n    }\n\n    steps {\n        copyArtifacts(buildWarJob.name) {\n            includePatterns('proxy/target/*.war')\n            buildSelector {\n                buildNumber('${WAR_BUILD_NUMBER}')\n            }\n        }\n        shell('ansible-playbook -e \"workspace=/tmp/e2e/${JOB_NAME}/${BUILD_NUMBER}\" testing/start.yml')\n        shell('ansible-playbook -e \"workspace=/tmp/e2e/${JOB_NAME}/${BUILD_NUMBER}\" testing/test.yml')\n        shell('ansible-playbook -e \"workspace=/tmp/e2e/${JOB_NAME}/${BUILD_NUMBER}\" testing/stop.yml')\n    }\n\n    publishers {\n        archiveJunit('testing/target/surefire-reports/*.xml')\n    }\n}\n\nsonarJob.with {\n\n    label('maven')\n    scm {\n        git {\n            remote {\n                github(repo, 'https', 'github.com')\n                refspec('${GIT_REFSPEC}')\n            }\n            branch('${GIT_COMMIT}')\n            localBranch('master')\n        }\n    }\n\n    publishers {\n        sonar()\n    }\n}\n\nsonarIncrJob.with {\n\n    label('maven')\n    scm {\n        git {\n            remote {\n                github(repo, 'https', 'github.com')\n                refspec('${GIT_REFSPEC}')\n            }\n            branch('${GIT_COMMIT}')\n            localBranch('master')\n        }\n    }\n\n    configure {\n        it / 'publishers' / 'hudson.plugins.sonar.SonarPublisher' {\n            jdk('(Inherit From Job)')\n            branch()\n            language()\n            jobAdditionalProperties('-Dsonar.analysis.mode=incremental -Dsonar.github.pullRequest=${ghprbPullId} -Dsonar.github.repository=' + repo)\n            settings(class: 'jenkins.mvn.DefaultSettingsProvider')\n            globalSettings(class: 'jenkins.mvn.DefaultGlobalSettingsProvider')\n            usePrivateRepository(false)\n        }\n    }\n}\n\ndeployJob.with {\n\n    label('maven')\n\n    scm {\n        git {\n            remote {\n                github(repo, 'https', 'github.com')\n                refspec('${GIT_REFSPEC}')\n            }\n            branch('${GIT_COMMIT}')\n            localBranch('master')\n        }\n    }\n\n    goals('clean deploy')\n\n}\n\npullRequestJob.with {\n\n    label('master')\n    displayName('Grid Router Pull Requests Flow')\n\n    scm {\n        git {\n            remote {\n                github(repo, 'https', 'github.com')\n                refspec('+refs/pull/*:refs/remotes/origin/pr/*')\n            }\n            branch('${sha1}')\n        }\n    }\n\n    triggers {\n        pullRequest {\n            orgWhitelist(['seleniumkit'])\n            permitAll()\n            useGitHubHooks()\n        }\n    }\n\n    steps {\n        phase('Build war file') {\n            job(sonarIncrJob.name) {\n                prop('GIT_REFSPEC', '+refs/pull/*:refs/remotes/origin/pr/*');\n                prop('GIT_COMMIT', '\\${sha1}');\n            }\n            job(buildWarJob.name) {\n                prop('GIT_REFSPEC', '+refs/pull/*:refs/remotes/origin/pr/*');\n                prop('GIT_COMMIT', '\\${sha1}');\n            }\n        }\n        phase('Run e2e tests', 'UNSTABLE') {\n            job(e2eTestsJob.name) {\n                prop('WAR_BUILD_NUMBER', '\\${' + buildWarJob.name.toUpperCase().replace(\"-\", \"_\") + '_BUILD_NUMBER}');\n                prop('GIT_REFSPEC', '+refs/pull/*:refs/remotes/origin/pr/*');\n                prop('GIT_COMMIT', '\\${sha1}');\n            }\n        }\n    }\n\n    publishers {\n        aggregateDownstreamTestResults()\n    }\n}\n\nsnapshotJob.with {\n\n    label('default')\n    displayName('Grid Router Snapshot Flow')\n\n    scm {\n        git {\n            remote {\n                github(repo, 'https', 'github.com')\n            }\n            localBranch('master')\n            branch('master')\n        }\n    }\n    triggers {\n        githubPush()\n    }\n\n    steps {\n        phase('Build war file') {\n            job(buildWarJob.name) {\n                prop('GIT_COMMIT', '\\${GIT_COMMIT}');\n                prop('GIT_REFSPEC', '');\n            }\n        }\n        phase('Run e2e tests') {\n            job(e2eTestsJob.name) {\n                prop('WAR_BUILD_NUMBER', '\\${' + buildWarJob.name.toUpperCase().replace(\"-\", \"_\") + '_BUILD_NUMBER}');\n                prop('GIT_COMMIT', '\\${GIT_COMMIT}');\n                prop('GIT_REFSPEC', '');\n            }\n            job(sonarJob.name) {\n                prop('GIT_COMMIT', '\\${GIT_COMMIT}');\n                prop('GIT_REFSPEC', '');\n            }\n        }\n        phase('Deploy war') {\n            job(deployJob.name) {\n                prop('GIT_COMMIT', '\\${GIT_COMMIT}');\n                prop('GIT_REFSPEC', '');\n            }\n        }\n    }\n\n    publishers {\n        aggregateDownstreamTestResults()\n    }\n}\n\nreleaseJob.with {\n\n    label('maven')\n    displayName('Grid Router Release Flow')\n\n    scm {\n        git {\n            remote {\n                github(repo, 'https', 'github.com')\n            }\n            localBranch('master')\n            branch('master')\n        }\n    }\n\n    goals('clean deploy')\n\n    wrappers {\n        mavenRelease {\n            releaseGoals('release:clean release:prepare release:perform')\n            dryRunGoals('-DdryRun=true release:prepare')\n            numberOfReleaseBuildsToKeep(10)\n        }\n    }\n}\n\nlistView(project) {\n    jobs {\n        regex(\"${project}_.*_flow\")\n    }\n\n    columns {\n        status()\n        name()\n        lastSuccess()\n        lastFailure()\n        lastDuration()\n        buildButton()\n    }\n}"
  },
  {
    "path": "config/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <groupId>ru.qatools.seleniumkit</groupId>\n        <artifactId>gridrouter</artifactId>\n        <version>1.32-SNAPSHOT</version>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <artifactId>gridrouter-config</artifactId>\n    <name>Selenium Grid Router Config</name>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.jvnet.jaxb2.maven2</groupId>\n                <artifactId>maven-jaxb2-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n\n    <dependencies>\n        <dependency>\n            <groupId>commons-io</groupId>\n            <artifactId>commons-io</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-lang3</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>commons-codec</groupId>\n            <artifactId>commons-codec</artifactId>\n            <version>1.10</version>\n        </dependency>\n\n        <!-- Tests -->\n        <dependency>\n            <groupId>junit</groupId>\n            <artifactId>junit</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.hamcrest</groupId>\n            <artifactId>hamcrest-all</artifactId>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n</project>\n"
  },
  {
    "path": "config/src/main/java/ru/qatools/gridrouter/config/GridRouterException.java",
    "content": "package ru.qatools.gridrouter.config;\n\n/**\n * @author Dmitry Baev charlie@yandex-team.ru\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic class GridRouterException extends RuntimeException {\n\n    public GridRouterException(String message, Throwable cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "config/src/main/java/ru/qatools/gridrouter/config/HostSelectionStrategy.java",
    "content": "package ru.qatools.gridrouter.config;\n\nimport java.util.List;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic interface HostSelectionStrategy {\n\n    Region selectRegion(List<Region> allRegions, List<Region> unvisitedRegions);\n\n    Host selectHost(List<Host> hosts);\n\n}\n"
  },
  {
    "path": "config/src/main/java/ru/qatools/gridrouter/config/RandomHostSelectionStrategy.java",
    "content": "package ru.qatools.gridrouter.config;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static java.util.Collections.nCopies;\nimport static java.util.Collections.shuffle;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic class RandomHostSelectionStrategy implements HostSelectionStrategy {\n\n    protected <T extends WithCount> T selectRandom(List<T> elements) {\n        List<T> copies = new ArrayList<>();\n        for (T element : elements) {\n            copies.addAll(nCopies(element.getCount(), element));\n        }\n        shuffle(copies);\n        return copies.get(0);\n    }\n\n    @Override\n    public Region selectRegion(List<Region> allRegions, List<Region> unvisitedRegions) {\n        return selectRandom(unvisitedRegions);\n    }\n\n    @Override\n    public Host selectHost(List<Host> hosts) {\n        return selectRandom(hosts);\n    }\n}\n"
  },
  {
    "path": "config/src/main/java/ru/qatools/gridrouter/config/RegionWithCount.java",
    "content": "package ru.qatools.gridrouter.config;\n\nimport java.util.List;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic interface RegionWithCount extends WithCount {\n\n    List<Host> getHosts();\n\n    @Override\n    default int getCount() {\n        return getHosts().stream().mapToInt(Host::getCount).sum();\n    }\n}\n"
  },
  {
    "path": "config/src/main/java/ru/qatools/gridrouter/config/SequentialHostSelectionStrategy.java",
    "content": "package ru.qatools.gridrouter.config;\n\nimport java.util.List;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic class SequentialHostSelectionStrategy implements HostSelectionStrategy {\n\n    private int hostIndex;\n\n    @Override\n    public Region selectRegion(List<Region> allRegions, List<Region> unvisitedRegions) {\n        return unvisitedRegions.get(0);\n    }\n\n    @Override\n    public Host selectHost(List<Host> hosts) {\n        Host host = hosts.get(hostIndex++ % hosts.size());\n        hostIndex %= hosts.size();\n        return host;\n    }\n}\n"
  },
  {
    "path": "config/src/main/java/ru/qatools/gridrouter/config/VersionWithCount.java",
    "content": "package ru.qatools.gridrouter.config;\n\nimport java.util.List;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic interface VersionWithCount extends WithCount {\n\n    List<Region> getRegions();\n\n    @Override\n    default int getCount() {\n        return getRegions().stream().mapToInt(Region::getCount).sum();\n    }\n}\n"
  },
  {
    "path": "config/src/main/java/ru/qatools/gridrouter/config/WithBrowserVersionFind.java",
    "content": "package ru.qatools.gridrouter.config;\n\nimport java.util.List;\n\nimport static org.apache.commons.lang3.StringUtils.isEmpty;\n\n/**\n * @author Dmitry Baev charlie@yandex-team.ru\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic interface WithBrowserVersionFind {\n\n    List<Browser> getBrowsers();\n\n    default Browser findBrowser(String name) {\n        return getBrowsers().stream()\n                .filter(b -> b.getName().equals(name))\n                .findFirst().orElse(null);\n    }\n\n    default Version find(String browserName, String browserVersion) {\n        Browser browser = findBrowser(browserName);\n\n        if (browser == null) {\n            return null;\n        }\n\n        return isEmpty(browserVersion) ?\n               browser.findDefaultVersion() :\n               browser.findVersion(browserVersion);\n    }\n}\n"
  },
  {
    "path": "config/src/main/java/ru/qatools/gridrouter/config/WithCopy.java",
    "content": "package ru.qatools.gridrouter.config;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * @author Dmitry Baev charlie@yandex-team.ru\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic interface WithCopy {\n\n    List<Host> getHosts();\n\n    String getName();\n\n    /**\n     * Creates a copy for the {@link Region} object, which is almost deep:\n     * the hosts itself are not copied although the list is new.\n     *\n     * @return a copy of the object\n     */\n    default Region copy() {\n        return new Region(new ArrayList<>(getHosts()), getName());\n    }\n}\n"
  },
  {
    "path": "config/src/main/java/ru/qatools/gridrouter/config/WithCount.java",
    "content": "package ru.qatools.gridrouter.config;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic interface WithCount {\n\n    int getCount();\n}\n"
  },
  {
    "path": "config/src/main/java/ru/qatools/gridrouter/config/WithRoute.java",
    "content": "package ru.qatools.gridrouter.config;\n\nimport org.apache.commons.codec.digest.DigestUtils;\n\nimport java.nio.charset.StandardCharsets;\n\n/**\n * @author Dmitry Baev charlie@yandex-team.ru\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic interface WithRoute {\n\n    default String getAddress() {\n        return getName() + \":\" + getPort();\n    }\n\n    default String getRoute() {\n        return \"http://\" + getAddress();\n    }\n\n    default String getRouteId() {\n        return DigestUtils.md5Hex(getRoute().getBytes(StandardCharsets.UTF_8));\n    }\n\n    String getName();\n\n    int getPort();\n}\n"
  },
  {
    "path": "config/src/main/java/ru/qatools/gridrouter/config/WithRoutesMap.java",
    "content": "package ru.qatools.gridrouter.config;\n\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * @author Dmitry Baev charlie@yandex-team.ru\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic interface WithRoutesMap {\n\n    List<Browser> getBrowsers();\n\n    default Map<String, String> getRoutesMap() {\n        HashMap<String, String> routes = new HashMap<>();\n        getBrowsers().stream()\n                .flatMap(b -> b.getVersions().stream())\n                .flatMap(v -> v.getRegions().stream())\n                .flatMap(r -> r.getHosts().stream())\n                .forEach(h -> routes.put(h.getRouteId(), h.getRoute()));\n        return routes;\n    }\n}\n"
  },
  {
    "path": "config/src/main/java/ru/qatools/gridrouter/config/WithVersionFind.java",
    "content": "package ru.qatools.gridrouter.config;\n\nimport java.util.List;\n\n/**\n * @author Dmitry Baev charlie@yandex-team.ru\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic interface WithVersionFind {\n\n    default Version findDefaultVersion() {\n        return findVersion(getDefaultVersion());\n    }\n\n    default Version findVersion(String versionPrefix) {\n        return getVersions().stream()\n                .filter(v -> v.getNumber().startsWith(versionPrefix))\n                .findFirst().orElse(null);\n    }\n\n    List<Version> getVersions();\n\n    String getDefaultVersion();\n}\n"
  },
  {
    "path": "config/src/main/java/ru/qatools/gridrouter/config/WithXmlView.java",
    "content": "package ru.qatools.gridrouter.config;\n\nimport javax.xml.bind.JAXBContext;\nimport javax.xml.bind.JAXBException;\nimport javax.xml.bind.Marshaller;\nimport java.io.StringWriter;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static javax.xml.bind.Marshaller.JAXB_ENCODING;\nimport static javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT;\n\n/**\n * @author Dmitry Baev charlie@yandex-team.ru\n */\npublic interface WithXmlView {\n\n    default String toXml() {\n        try {\n            JAXBContext context = JAXBContext.newInstance(getClass());\n            Marshaller marshaller = context.createMarshaller();\n            marshaller.setProperty(JAXB_ENCODING, UTF_8.toString());\n            marshaller.setProperty(JAXB_FORMATTED_OUTPUT, true);\n            StringWriter writer = new StringWriter();\n            marshaller.marshal(this, writer);\n            return writer.toString();\n        } catch (JAXBException e) {\n            throw new GridRouterException(\"Unable to marshall bean\", e);\n        }\n    }\n}\n"
  },
  {
    "path": "config/src/main/resources/xsd/bindings.xjb",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<jaxb:bindings\n        xmlns:jaxb=\"http://java.sun.com/xml/ns/jaxb\"\n        xmlns:xs=\"http://www.w3.org/2001/XMLSchema\"\n        xmlns:xjc=\"http://java.sun.com/xml/ns/jaxb/xjc\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://java.sun.com/xml/ns/jaxb http://java.sun.com/xml/ns/jaxb/bindingschema_2_1.xsd\"\n        xmlns:inheritance=\"http://jaxb2-commons.dev.java.net/basic/inheritance\"\n        jaxb:extensionBindingPrefixes=\"xjc annox\"\n        schemaLocation=\"config.xsd\"\n        version=\"2.1\">\n\n    <jaxb:bindings node=\"//xs:complexType[@name='Browsers']\">\n        <inheritance:implements>ru.qatools.gridrouter.config.WithBrowserVersionFind</inheritance:implements>\n        <inheritance:implements>ru.qatools.gridrouter.config.WithXmlView</inheritance:implements>\n        <inheritance:implements>ru.qatools.gridrouter.config.WithRoutesMap</inheritance:implements>\n    </jaxb:bindings>\n\n    <jaxb:bindings node=\"//xs:complexType[@name='Browser']\">\n        <inheritance:implements>ru.qatools.gridrouter.config.WithVersionFind</inheritance:implements>\n    </jaxb:bindings>\n\n    <jaxb:bindings node=\"//xs:complexType[@name='Version']\">\n        <inheritance:implements>ru.qatools.gridrouter.config.VersionWithCount</inheritance:implements>\n    </jaxb:bindings>\n\n    <jaxb:bindings node=\"//xs:complexType[@name='Region']\">\n        <inheritance:implements>ru.qatools.gridrouter.config.WithCopy</inheritance:implements>\n        <inheritance:implements>ru.qatools.gridrouter.config.RegionWithCount</inheritance:implements>\n    </jaxb:bindings>\n\n    <jaxb:bindings node=\"//xs:complexType[@name='Host']\">\n        <inheritance:implements>ru.qatools.gridrouter.config.WithRoute</inheritance:implements>\n        <inheritance:implements>ru.qatools.gridrouter.config.WithCount</inheritance:implements>\n    </jaxb:bindings>\n\n    <jaxb:globalBindings>\n        <jaxb:serializable/>\n        <xjc:simple/>\n    </jaxb:globalBindings>\n\n</jaxb:bindings>\n"
  },
  {
    "path": "config/src/main/resources/xsd/config.xsd",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<xsd:schema attributeFormDefault=\"unqualified\" elementFormDefault=\"unqualified\"\n            xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n            xmlns:qa=\"urn:config.gridrouter.qatools.ru\"\n            targetNamespace=\"urn:config.gridrouter.qatools.ru\">\n\n    <xsd:element name=\"browsers\" type=\"qa:Browsers\"/>\n\n    <xsd:complexType name=\"Browsers\">\n        <xsd:sequence>\n            <xsd:element name=\"browser\" type=\"qa:Browser\" minOccurs=\"0\" maxOccurs=\"unbounded\"/>\n        </xsd:sequence>\n    </xsd:complexType>\n\n    <xsd:complexType name=\"Browser\">\n        <xsd:sequence>\n            <xsd:element name=\"version\" type=\"qa:Version\" minOccurs=\"0\" maxOccurs=\"unbounded\"/>\n        </xsd:sequence>\n        <xsd:attribute name=\"name\" type=\"xsd:string\" use=\"required\"/>\n        <xsd:attribute name=\"defaultVersion\" type=\"xsd:string\" use=\"required\"/>\n    </xsd:complexType>\n\n    <xsd:complexType name=\"Version\">\n        <xsd:sequence>\n            <xsd:element name=\"region\" type=\"qa:Region\" minOccurs=\"0\" maxOccurs=\"unbounded\"/>\n        </xsd:sequence>\n        <xsd:attribute name=\"number\" type=\"xsd:string\" use=\"required\"/>\n        <xsd:attribute name=\"permittedCount\" type=\"xsd:int\" use=\"optional\"/>\n    </xsd:complexType>\n\n    <xsd:complexType name=\"Region\">\n        <xsd:sequence>\n            <xsd:element name=\"host\" type=\"qa:Host\" minOccurs=\"0\" maxOccurs=\"unbounded\"/>\n        </xsd:sequence>\n        <xsd:attribute name=\"name\" type=\"xsd:string\" use=\"required\"/>\n    </xsd:complexType>\n\n    <xsd:complexType name=\"Host\">\n        <xsd:attribute name=\"name\" type=\"xsd:string\" use=\"required\"/>\n        <xsd:attribute name=\"port\" type=\"xsd:int\" use=\"required\"/>\n        <xsd:attribute name=\"count\" type=\"xsd:int\" use=\"required\"/>\n    </xsd:complexType>\n</xsd:schema>\n"
  },
  {
    "path": "config/src/test/java/ru/qatools/gridrouter/config/RandomHostSelectionStrategyTest.java",
    "content": "package ru.qatools.gridrouter.config;\n\nimport org.hamcrest.Matcher;\nimport org.junit.Test;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.UUID;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.both;\nimport static org.hamcrest.Matchers.greaterThan;\nimport static org.hamcrest.Matchers.lessThan;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic class RandomHostSelectionStrategyTest {\n\n    private static final double ALLOWED_DEVIATION = 0.01;\n\n    @Test\n    @SuppressWarnings(\"ArraysAsListWithZeroOrOneArgument\")\n    public void testRandomness() {\n        int entriesCount = 5000000;\n        int keysCount = 10;\n\n        Host host1 = new Host(\"host_1\", 4444, keysCount - 1);\n\n        List<Host> hosts = new ArrayList<>(keysCount);\n        hosts.add(host1);\n\n        int i = keysCount;\n        while (i --> 1) {\n            hosts.add(newHost());\n        }\n\n        HashMap<Host, Integer> appearances = new HashMap<>(keysCount, entriesCount / keysCount);\n\n        RandomHostSelectionStrategy strategy = new RandomHostSelectionStrategy();\n        i = entriesCount;\n        while (i-- > 0) {\n            Host host = strategy.selectRandom(hosts);\n            appearances.put(host, Optional.ofNullable(appearances.get(host)).orElse(0) + 1);\n        }\n\n        assertThat(appearances.remove(host1), isAround(entriesCount / 2));\n\n        for (int count : appearances.values()) {\n            assertThat(count, isAround(entriesCount / 2 / (keysCount - 1)));\n        }\n    }\n\n    private static Host newHost() {\n        return new Host(UUID.randomUUID().toString(), 4444, 1);\n    }\n\n    private static Matcher<Integer> isAround(int count) {\n        return both(greaterThan(\n                (int) (count * (1 - ALLOWED_DEVIATION))\n        )).and(lessThan(\n                (int) (count * (1 + ALLOWED_DEVIATION))\n        ));\n    }\n}\n"
  },
  {
    "path": "pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <parent>\n        <groupId>org.sonatype.oss</groupId>\n        <artifactId>oss-parent</artifactId>\n        <version>9</version>\n    </parent>\n\n    <groupId>ru.qatools.seleniumkit</groupId>\n    <artifactId>gridrouter</artifactId>\n    <version>1.32-SNAPSHOT</version>\n    <packaging>pom</packaging>\n\n    <modules>\n        <module>config</module>\n        <module>proxy</module>\n    </modules>\n\n    <name>Selenium Grid Router</name>\n\n    <properties>\n        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n        <java.version>1.8</java.version>\n\n        <spring.version>4.1.6.RELEASE</spring.version>\n        <jetty.version>9.3.6.v20151106</jetty.version>\n        <jackson.version>2.6.0-rc3</jackson.version>\n        <slf4j.version>1.7.7</slf4j.version>\n    </properties>\n\n    <organization>\n        <name>Yandex</name>\n        <url>http://yandex.ru/</url>\n    </organization>\n\n    <licenses>\n        <license>\n            <name>The Apache Software License, Version 2.0</name>\n            <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>\n            <distribution>repo</distribution>\n        </license>\n    </licenses>\n\n    <scm>\n        <connection>scm:git:git@github.com:seleniumkit/gridrouter.git</connection>\n        <developerConnection>scm:git:git@github.com:seleniumkit/gridrouter.git</developerConnection>\n        <url>https://github.com/seleniumkit/gridrouter</url>\n        <tag>HEAD</tag>\n    </scm>\n\n    <issueManagement>\n        <system>GitHub Issues</system>\n        <url>https://github.com/seleniumkit/gridrouter/issues</url>\n    </issueManagement>\n\n    <ciManagement>\n        <system>Jenkins</system>\n        <url>http://ci.qatools.ru/</url>\n    </ciManagement>\n\n    <dependencyManagement>\n        <dependencies>\n            <dependency>\n                <groupId>commons-io</groupId>\n                <artifactId>commons-io</artifactId>\n                <version>2.4</version>\n            </dependency>\n            <dependency>\n                <groupId>org.apache.commons</groupId>\n                <artifactId>commons-lang3</artifactId>\n                <version>3.4</version>\n            </dependency>\n\n            <dependency>\n                <groupId>junit</groupId>\n                <artifactId>junit</artifactId>\n                <version>4.12</version>\n            </dependency>\n            <dependency>\n                <groupId>org.hamcrest</groupId>\n                <artifactId>hamcrest-all</artifactId>\n                <version>1.3</version>\n            </dependency>\n        </dependencies>\n    </dependencyManagement>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-compiler-plugin</artifactId>\n                <version>3.3</version>\n                <configuration>\n                    <source>${java.version}</source>\n                    <target>${java.version}</target>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-javadoc-plugin</artifactId>\n                <version>2.10.3</version>\n                <configuration>\n                    <additionalparam>-Xdoclint:none</additionalparam>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-source-plugin</artifactId>\n                <version>2.4</version>\n                <executions>\n                    <execution>\n                        <goals>\n                            <goal>jar</goal>\n                        </goals>\n                    </execution>\n                </executions>\n            </plugin>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-release-plugin</artifactId>\n                <version>2.5.2</version>\n                <configuration>\n                    <tagNameFormat>@{project.version}</tagNameFormat>\n                </configuration>\n            </plugin>\n        </plugins>\n        <pluginManagement>\n            <plugins>\n                <plugin>\n                    <groupId>org.jvnet.jaxb2.maven2</groupId>\n                    <artifactId>maven-jaxb2-plugin</artifactId>\n                    <version>0.12.3</version>\n                    <executions>\n                        <execution>\n                            <goals>\n                                <goal>generate</goal>\n                            </goals>\n                        </execution>\n                    </executions>\n                    <configuration>\n                        <bindingDirectory>src/main/resources/xsd</bindingDirectory>\n                        <schemaDirectory>src/main/resources/xsd</schemaDirectory>\n                        <removeOldOutput>true</removeOldOutput>\n                        <readOnly>true</readOnly>\n                        <verbose>true</verbose>\n                        <args>\n                            <arg>-Xinheritance</arg>\n                            <arg>-Xvalue-constructor</arg>\n                        </args>\n                        <plugins>\n                            <plugin>\n                                <groupId>org.jvnet.jaxb2_commons</groupId>\n                                <artifactId>jaxb2-basics</artifactId>\n                                <version>0.9.4</version>\n                            </plugin>\n                            <plugin>\n                                <groupId>org.jvnet.jaxb2_commons</groupId>\n                                <artifactId>jaxb2-value-constructor</artifactId>\n                                <version>3.0</version>\n                            </plugin>\n                        </plugins>\n                    </configuration>\n                </plugin>\n            </plugins>\n        </pluginManagement>\n    </build>\n\n</project>\n"
  },
  {
    "path": "proxy/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <parent>\n        <groupId>ru.qatools.seleniumkit</groupId>\n        <artifactId>gridrouter</artifactId>\n        <version>1.32-SNAPSHOT</version>\n    </parent>\n    <modelVersion>4.0.0</modelVersion>\n\n    <artifactId>gridrouter-proxy</artifactId>\n    <name>Selenium Grid Router Proxy</name>\n    <packaging>war</packaging>\n\n    <build>\n        <plugins>\n            <plugin>\n                <groupId>org.apache.maven.plugins</groupId>\n                <artifactId>maven-war-plugin</artifactId>\n                <configuration>\n                    <attachClasses>true</attachClasses>\n                </configuration>\n            </plugin>\n            <plugin>\n                <groupId>org.jvnet.jaxb2.maven2</groupId>\n                <artifactId>maven-jaxb2-plugin</artifactId>\n            </plugin>\n        </plugins>\n    </build>\n\n    <dependencies>\n        <dependency>\n            <groupId>ru.qatools.seleniumkit</groupId>\n            <artifactId>gridrouter-config</artifactId>\n            <version>${project.version}</version>\n        </dependency>\n\n        <dependency>\n            <groupId>ru.yandex.qatools.beanloader</groupId>\n            <artifactId>beanloader</artifactId>\n            <version>2.1</version>\n        </dependency>\n\n        <!-- Commons -->\n        <dependency>\n            <groupId>commons-io</groupId>\n            <artifactId>commons-io</artifactId>\n        </dependency>\n        <dependency>\n            <groupId>org.apache.commons</groupId>\n            <artifactId>commons-lang3</artifactId>\n        </dependency>\n\n        <!-- Spring -->\n        <dependency>\n            <groupId>org.springframework</groupId>\n            <artifactId>spring-web</artifactId>\n            <version>${spring.version}</version>\n        </dependency>\n\n        <!-- Jetty -->\n        <dependency>\n            <groupId>org.eclipse.jetty</groupId>\n            <artifactId>jetty-servlet</artifactId>\n            <version>${jetty.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.jetty</groupId>\n            <artifactId>jetty-proxy</artifactId>\n            <version>${jetty.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.jetty</groupId>\n            <artifactId>jetty-security</artifactId>\n            <version>${jetty.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.eclipse.jetty</groupId>\n            <artifactId>jetty-annotations</artifactId>\n            <version>${jetty.version}</version>\n            <scope>test</scope>\n        </dependency>\n\n        <!-- Jackson -->\n        <dependency>\n        \t<groupId>com.fasterxml.jackson.core</groupId>\n        \t<artifactId>jackson-databind</artifactId>\n        \t<version>${jackson.version}</version>\n        </dependency>\n        <dependency>\n        \t<groupId>com.fasterxml.jackson.core</groupId>\n        \t<artifactId>jackson-annotations</artifactId>\n        \t<version>${jackson.version}</version>\n        </dependency>\n\n        <!-- Http Client -->\n        <dependency>\n            <groupId>org.apache.httpcomponents</groupId>\n            <artifactId>httpclient</artifactId>\n            <version>4.5.2</version>\n        </dependency>\n\n        <!-- slf4j -->\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-api</artifactId>\n            <version>${slf4j.version}</version>\n        </dependency>\n        <dependency>\n            <groupId>org.slf4j</groupId>\n            <artifactId>slf4j-log4j12</artifactId>\n            <version>${slf4j.version}</version>\n        </dependency>\n\n        <!-- Tests -->\n        <dependency>\n            <groupId>junit</groupId>\n            <artifactId>junit</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.hamcrest</groupId>\n            <artifactId>hamcrest-all</artifactId>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.mockito</groupId>\n            <artifactId>mockito-all</artifactId>\n            <version>1.9.5</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.mock-server</groupId>\n            <artifactId>mockserver-netty</artifactId>\n            <version>3.9.17</version>\n            <scope>test</scope>\n            <exclusions>\n                <exclusion>\n                    <artifactId>logback-classic</artifactId>\n                    <groupId>ch.qos.logback</groupId>\n                </exclusion>\n            </exclusions>\n        </dependency>\n        <dependency>\n            <groupId>org.json</groupId>\n            <artifactId>json</artifactId>\n            <version>20140107</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.seleniumhq.selenium</groupId>\n            <artifactId>selenium-java</artifactId>\n            <version>2.53.0</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>xml-apis</groupId>\n            <artifactId>xml-apis</artifactId>\n            <version>1.4.01</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>ru.yandex.qatools.matchers</groupId>\n            <artifactId>matcher-decorators</artifactId>\n            <version>1.1</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.springframework</groupId>\n            <artifactId>spring-test</artifactId>\n            <version>${spring.version}</version>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n</project>\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/ConfigRepository.java",
    "content": "package ru.qatools.gridrouter;\n\nimport ru.qatools.gridrouter.config.Browser;\nimport ru.qatools.gridrouter.config.Browsers;\nimport ru.qatools.gridrouter.config.Version;\nimport ru.qatools.gridrouter.json.JsonCapabilities;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * @author Ilya Sadykov\n */\npublic interface ConfigRepository {\n    Map<String, Browsers> getQuotaMap();\n\n    String getRoute(String routeId);\n\n    default Version findVersion(String user, JsonCapabilities caps) {\n        final Browsers browsers = getQuotaMap().get(user);\n        return browsers != null ? browsers.find(caps.getBrowserName(), caps.getVersion()) : null;\n    }\n\n    default Map<String, Integer> getBrowsersCountMap(String user) {\n        HashMap<String, Integer> countMap = new HashMap<>();\n        for (Browser browser : getQuotaMap().get(user).getBrowsers()) {\n            for (Version version : browser.getVersions()) {\n                countMap.put(browser.getName() + \":\" + version.getNumber(), version.getCount());\n            }\n        }\n        return countMap;\n    }\n}\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/ConfigRepositoryXml.java",
    "content": "package ru.qatools.gridrouter;\n\nimport org.apache.commons.io.FilenameUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Value;\nimport ru.qatools.beanloader.BeanChangeListener;\nimport ru.qatools.beanloader.BeanLoader;\nimport ru.qatools.beanloader.BeanWatcher;\nimport ru.qatools.gridrouter.config.Browsers;\n\nimport javax.annotation.PostConstruct;\nimport java.io.File;\nimport java.io.IOException;\nimport java.nio.file.Path;\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * @author Alexander Andyashin aandryashin@yandex-team.ru\n * @author Dmitry Baev charlie@yandex-team.ru\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic class ConfigRepositoryXml implements ConfigRepository, BeanChangeListener<Browsers> {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(ConfigRepositoryXml.class);\n\n    private static final String XML_GLOB = \"*.xml\";\n\n    @Value(\"${grid.config.quota.directory}\")\n    private File quotaDirectory;\n\n    @Value(\"${grid.config.quota.hotReload}\")\n    private boolean quotaHotReload;\n\n    private Map<String, Browsers> userBrowsers = new HashMap<>();\n\n    private Map<String, String> routes = new HashMap<>();\n\n    @PostConstruct\n    public void init() {\n        try {\n            if (quotaHotReload) {\n                LOGGER.debug(\"Starting quota watcher\");\n                BeanWatcher.watchFor(Browsers.class, quotaDirectory.toPath(), XML_GLOB, this);\n            } else {\n                LOGGER.debug(\"Loading quota configuration\");\n                BeanLoader.loadAll(Browsers.class, quotaDirectory.toPath(), XML_GLOB, this);\n            }\n        } catch (IOException e) {\n            LOGGER.error(\"Quota configuration loading failed\", e);\n        }\n    }\n\n    @Override\n    public void beanChanged(Path filename, Browsers browsers) {\n        if (browsers == null) {\n            LOGGER.info(\"Configuration file [{}] was deleted. \"\n                    + \"It is not purged from the running gridrouter process though.\", filename);\n        } else {\n            LOGGER.info(\"Loading quota configuration file [{}]\", filename);\n            String user = FilenameUtils.getBaseName(filename.toString());\n            userBrowsers.put(user, browsers);\n            routes.putAll(browsers.getRoutesMap());\n            LOGGER.info(\"Loaded quota configuration for [{}] from [{}]: \\n\\n{}\",\n                    user, filename, browsers.toXml());\n        }\n    }\n\n    @Override\n    public Map<String, Browsers> getQuotaMap() {\n        return userBrowsers;\n    }\n\n    @Override\n    public String getRoute(String routeId) {\n        return routes.get(routeId);\n    }\n}\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/JsonWireUtils.java",
    "content": "package ru.qatools.gridrouter;\n\nimport org.apache.http.client.utils.URIBuilder;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport javax.servlet.http.HttpServletRequest;\nimport java.io.UnsupportedEncodingException;\nimport java.net.URISyntaxException;\nimport java.net.URLDecoder;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static org.springframework.http.HttpMethod.DELETE;\n\n/**\n * @author Alexander Andyashin aandryashin@yandex-team.ru\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n * @author Dmitry Baev charlie@yandex-team.ru\n * @author Artem Eroshenko eroshenkoam@yandex-team.ru\n */\npublic final class JsonWireUtils {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(JsonWireUtils.class);\n\n    public static final String WD_HUB_SESSION = \"/wd/hub/session/\";\n\n    public static final int SESSION_HASH_LENGTH = 32;\n\n    private JsonWireUtils() {\n    }\n\n    public static boolean isUriValid(String uri) {\n        return uri.length() > getUriPrefixLength();\n    }\n\n    public static boolean isSessionDeleteRequest(HttpServletRequest request, String command) {\n        return DELETE.name().equalsIgnoreCase(request.getMethod()) && !command.contains(\"/\");\n    }\n\n    public static String getSessionHash(String uri) {\n        return uri.substring(WD_HUB_SESSION.length(), getUriPrefixLength());\n    }\n\n    public static String getFullSessionId(String uri) {\n        String tail = uri.substring(WD_HUB_SESSION.length());\n        int end = tail.indexOf('/');\n        if (end < 0) {\n            return tail;\n        }\n        return tail.substring(0, end);\n    }\n\n    public static int getUriPrefixLength() {\n        return WD_HUB_SESSION.length() + SESSION_HASH_LENGTH;\n    }\n\n    public static String redirectionUrl(String host, String command) throws URISyntaxException {\n        return new URIBuilder(host).setPath(WD_HUB_SESSION + command).build().toString();\n    }\n\n    public static String getCommand(String uri) {\n        String encodedCommand = uri.substring(getUriPrefixLength());\n        try {\n            return URLDecoder.decode(encodedCommand, UTF_8.name());\n        } catch (UnsupportedEncodingException e) {\n            LOGGER.error(\"[UNABLE_TO_DECODE_COMMAND] - could not decode command: {}\", encodedCommand, e);\n            return encodedCommand;\n        }\n    }\n}\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/PingServlet.java",
    "content": "package ru.qatools.gridrouter;\n\nimport javax.servlet.ServletException;\nimport javax.servlet.annotation.WebServlet;\nimport javax.servlet.http.HttpServlet;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\nimport java.io.PrintWriter;\n\n/**\n * @author Alexander Andyashin aandryashin@yandex-team.ru\n * @author Artem Eroshenko eroshenkoam@yandex-team.ru\n * @author Dmitry Baev charlie@yandex-team.ru\n */\n@WebServlet(urlPatterns = {\"/ping\"}, asyncSupported = true)\npublic class PingServlet extends HttpServlet {\n\n    @Override\n    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n        try (PrintWriter writer = resp.getWriter()) {\n            writer.print(\"OK\");\n            writer.flush();\n        }\n    }\n}\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/ProxyServlet.java",
    "content": "package ru.qatools.gridrouter;\n\nimport org.apache.commons.io.IOUtils;\nimport org.eclipse.jetty.client.api.Request;\nimport org.eclipse.jetty.client.util.StringContentProvider;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport ru.qatools.gridrouter.json.JsonMessage;\nimport ru.qatools.gridrouter.json.JsonMessageFactory;\nimport ru.qatools.gridrouter.sessions.StatsCounter;\n\nimport javax.servlet.ServletConfig;\nimport javax.servlet.ServletException;\nimport javax.servlet.annotation.WebInitParam;\nimport javax.servlet.annotation.WebServlet;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static org.springframework.web.context.support.SpringBeanAutowiringSupport.processInjectionBasedOnServletContext;\nimport static ru.qatools.gridrouter.JsonWireUtils.*;\nimport static ru.qatools.gridrouter.RequestUtils.getRemoteHost;\n\n/**\n * @author Alexander Andyashin aandryashin@yandex-team.ru\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n * @author Dmitry Baev charlie@yandex-team.ru\n * @author Artem Eroshenko eroshenkoam@yandex-team.ru\n */\n@WebServlet(\n        urlPatterns = {WD_HUB_SESSION + \"*\"},\n        asyncSupported = true,\n        initParams = {\n                @WebInitParam(name = \"timeout\", value = \"300000\"),\n                @WebInitParam(name = \"idleTimeout\", value = \"300000\")\n        }\n)\npublic class ProxyServlet extends org.eclipse.jetty.proxy.ProxyServlet {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(ProxyServlet.class);\n\n    @Autowired\n    private transient ConfigRepository config;\n\n    @Autowired\n    private transient StatsCounter statsCounter;\n\n    @Override\n    public void init(ServletConfig config) throws ServletException {\n        super.init(config);\n        processInjectionBasedOnServletContext(this, config.getServletContext());\n    }\n\n    @Override\n    protected void sendProxyRequest(\n            HttpServletRequest clientRequest, HttpServletResponse proxyResponse, Request proxyRequest) {\n        try {\n            Request request = getRequestWithoutSessionId(clientRequest, proxyRequest);\n            super.sendProxyRequest(clientRequest, proxyResponse, request);\n        } catch (Exception exception) {\n            LOGGER.error(\"[REQUEST_READ_FAILURE] [{}] - could not read client request, proxying request as is\",\n                    clientRequest.getRemoteHost(), exception);\n            super.sendProxyRequest(clientRequest, proxyResponse, proxyRequest);\n        }\n    }\n\n    @Override\n    protected String rewriteTarget(HttpServletRequest request) {\n        String uri = request.getRequestURI();\n\n        String remoteHost = getRemoteHost(request);\n\n        if (!isUriValid(uri)) {\n            LOGGER.warn(\"[INVALID_SESSION_HASH] [{}] - request uri is {}\", remoteHost, uri);\n            return null;\n        }\n\n        String route = config.getRoute(getSessionHash(uri));\n        String command = getCommand(uri);\n\n        if (route == null) {\n            LOGGER.error(\"[ROUTE_NOT_FOUND] [{}] - request uri is {}\", remoteHost, uri);\n            return null;\n        }\n\n        if (isSessionDeleteRequest(request, command)) {\n            LOGGER.info(\"[SESSION_DELETED] [{}] [{}] [{}]\", remoteHost, route, command);\n            statsCounter.deleteSession(getFullSessionId(uri), route);\n        } else {\n            statsCounter.updateSession(getFullSessionId(uri), route);\n        }\n\n        try {\n            return redirectionUrl(route, command);\n        } catch (Exception exception) {\n            LOGGER.error(\"[REDIRECTION_URL_ERROR] [{}] - error building redirection uri because of {}\\n\"\n                            + \"    request uri:    {}\\n\"\n                            + \"    parsed route:   {}\\n\"\n                            + \"    parsed command: {}\",\n                    remoteHost, exception.toString(), uri, route,  command);\n        }\n        return null;\n    }\n\n    protected Request getRequestWithoutSessionId(HttpServletRequest clientRequest, Request proxyRequest) throws IOException {\n        String content = IOUtils.toString(clientRequest.getInputStream(), UTF_8);\n        if (!content.isEmpty()) {\n            String remoteHost = getRemoteHost(clientRequest);\n            content = removeSessionIdSafe(content, remoteHost);\n        }\n        return proxyRequest.content(\n                new StringContentProvider(clientRequest.getContentType(), content, UTF_8));\n    }\n\n    private String removeSessionIdSafe(String content, String remoteHost) {\n        try {\n            JsonMessage message = JsonMessageFactory.from(content);\n            message.setSessionId(null);\n            return message.toJson();\n        } catch (IOException exception) {\n            LOGGER.error(\"[UNABLE_TO_REMOVE_SESSION_ID] [{}] - could not create proxy request without session id, \"\n                            + \"proxying request as is. Request content is: {}\",\n                    remoteHost, content, exception);\n        }\n        return content;\n    }\n}\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/QuotaServlet.java",
    "content": "package ru.qatools.gridrouter;\n\nimport org.apache.commons.io.IOUtils;\nimport org.springframework.beans.factory.annotation.Autowired;\n\nimport javax.servlet.ServletException;\nimport javax.servlet.annotation.HttpConstraint;\nimport javax.servlet.annotation.ServletSecurity;\nimport javax.servlet.annotation.WebServlet;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\nimport java.io.OutputStream;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static javax.servlet.http.HttpServletResponse.SC_OK;\nimport static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;\nimport static ru.qatools.gridrouter.json.JsonFormatter.toJson;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\n@WebServlet(urlPatterns = {\"/quota\"}, asyncSupported = true)\n@ServletSecurity(value = @HttpConstraint(rolesAllowed = {\"user\"}))\npublic class QuotaServlet extends SpringHttpServlet {\n\n    @Autowired\n    private transient ConfigRepository config;\n\n    @Override\n    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n        resp.setStatus(SC_OK);\n        resp.setContentType(APPLICATION_JSON_VALUE);\n        try (OutputStream output = resp.getOutputStream()) {\n            String jsonResponse = toJson(config.getBrowsersCountMap(req.getRemoteUser()));\n            IOUtils.write(jsonResponse, output, UTF_8);\n        }\n    }\n}\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/RequestUtils.java",
    "content": "package ru.qatools.gridrouter;\n\nimport javax.servlet.http.HttpServletRequest;\n\npublic final class RequestUtils {\n\n    private static final String X_FORWARDED_FOR = \"X-Forwarded-For\";\n\n    public static String getRemoteHost(HttpServletRequest request) {\n        String remoteHost = request.getHeader(X_FORWARDED_FOR);\n        if (remoteHost == null) {\n            return request.getRemoteHost();\n        }\n        return remoteHost;\n    }\n    \n    private RequestUtils(){}\n}\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/RouteServlet.java",
    "content": "package ru.qatools.gridrouter;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport org.apache.commons.io.IOUtils;\nimport org.apache.http.HttpResponse;\nimport org.apache.http.client.config.RequestConfig;\nimport org.apache.http.client.methods.HttpPost;\nimport org.apache.http.entity.StringEntity;\nimport org.apache.http.impl.client.CloseableHttpClient;\nimport org.apache.http.impl.client.HttpClientBuilder;\nimport org.apache.http.impl.client.LaxRedirectStrategy;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport ru.qatools.gridrouter.caps.CapabilityProcessorFactory;\nimport ru.qatools.gridrouter.config.Host;\nimport ru.qatools.gridrouter.config.HostSelectionStrategy;\nimport ru.qatools.gridrouter.config.Region;\nimport ru.qatools.gridrouter.config.Version;\nimport ru.qatools.gridrouter.json.JsonCapabilities;\nimport ru.qatools.gridrouter.json.JsonMessage;\nimport ru.qatools.gridrouter.json.JsonMessageFactory;\nimport ru.qatools.gridrouter.sessions.AvailableBrowserCheckExeption;\nimport ru.qatools.gridrouter.sessions.AvailableBrowsersChecker;\nimport ru.qatools.gridrouter.sessions.StatsCounter;\n\nimport javax.servlet.ServletException;\nimport javax.servlet.annotation.HttpConstraint;\nimport javax.servlet.annotation.ServletSecurity;\nimport javax.servlet.annotation.WebServlet;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.time.Instant;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.Callable;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.ScheduledExecutorService;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.concurrent.atomic.AtomicLong;\n\nimport static java.lang.String.format;\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static java.util.stream.Collectors.toList;\nimport static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;\nimport static javax.servlet.http.HttpServletResponse.SC_OK;\nimport static org.apache.http.HttpHeaders.ACCEPT;\nimport static org.apache.http.entity.ContentType.APPLICATION_JSON;\nimport static ru.qatools.gridrouter.RequestUtils.getRemoteHost;\n\n/**\n * @author Alexander Andyashin aandryashin@yandex-team.ru\n * @author Dmitry Baev charlie@yandex-team.ru\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n * @author Artem Eroshenko eroshenkoam@yandex-team.ru\n */\n@WebServlet(urlPatterns = {\"/wd/hub/session\"}, asyncSupported = true)\n@ServletSecurity(value = @HttpConstraint(rolesAllowed = {\"user\"}))\npublic class RouteServlet extends SpringHttpServlet {\n\n    private static final Logger LOGGER = LoggerFactory.getLogger(RouteServlet.class);\n\n    private static final String ROUTE_TIMEOUT_CAPABILITY = \"grid.router.route.timeout.seconds\";\n\n    private static final int MAX_ROUTE_TIMEOUT_SECONDS = 300;\n\n    @Autowired\n    private transient ConfigRepository config;\n    @Autowired\n    private transient HostSelectionStrategy hostSelectionStrategy;\n    @Autowired\n    private transient StatsCounter statsCounter;\n    @Autowired\n    private transient CapabilityProcessorFactory capabilityProcessorFactory;\n    @Autowired\n    private transient AvailableBrowsersChecker avblBrowsersChecker;\n\n    @Value(\"${grid.router.route.timeout.seconds:120}\")\n    private int routeTimeout;\n\n    private AtomicLong requestCounter = new AtomicLong();\n\n    @Override\n    protected void doPost(HttpServletRequest request, HttpServletResponse response)\n            throws ServletException, IOException {\n        ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);\n        JsonMessage message = JsonMessageFactory.from(request.getInputStream());\n\n        long requestId = requestCounter.getAndIncrement();\n        int routeTimeout = getRouteTimeout(request.getRemoteUser(), message);\n        AtomicBoolean terminated = new AtomicBoolean(false);\n        executor.submit(getRouteCallable(request, message, response, requestId, routeTimeout, terminated));\n        executor.shutdown();\n        try {\n            executor.awaitTermination(routeTimeout, TimeUnit.SECONDS);\n            terminated.set(true);\n        } catch (InterruptedException e) {\n            executor.shutdownNow();\n        }\n        replyWithError(\"Timed out when searching for valid host\", response);\n    }\n\n    private Callable<Object> getRouteCallable(HttpServletRequest request, JsonMessage message, HttpServletResponse response,\n                                              long requestId, int routeTimeout, AtomicBoolean terminated) {\n        return () -> {\n            route(request, message, response, requestId, routeTimeout, terminated);\n            return null;\n        };\n    }\n\n    private int getRouteTimeout(String user, JsonMessage message) {\n        JsonCapabilities caps = message.getDesiredCapabilities();\n        try {\n            if (caps.any().containsKey(ROUTE_TIMEOUT_CAPABILITY)) {\n                Integer desiredRouteTimeout = Integer.valueOf(String.valueOf(caps.any().get(ROUTE_TIMEOUT_CAPABILITY)));\n                routeTimeout = (desiredRouteTimeout < MAX_ROUTE_TIMEOUT_SECONDS) ?\n                        desiredRouteTimeout :\n                        MAX_ROUTE_TIMEOUT_SECONDS;\n                LOGGER.warn(\"[{}] [INVALID_ROUTE_TIMEOUT] [{}]\", user, desiredRouteTimeout);\n            }\n        } catch (NumberFormatException ignored) {\n        }\n        return routeTimeout;\n    }\n\n    private void route(HttpServletRequest request, JsonMessage message,\n                       HttpServletResponse response,\n                       long requestId, int routeTimeout, AtomicBoolean terminated) throws IOException {\n\n\n        long initialSeconds = Instant.now().getEpochSecond();\n\n        JsonCapabilities caps = message.getDesiredCapabilities();\n\n        String user = request.getRemoteUser();\n        String remoteHost = getRemoteHost(request);\n        String browser = caps.describe();\n        Version actualVersion = config.findVersion(user, caps);\n\n        if (actualVersion == null) {\n            LOGGER.warn(\"[{}] [UNSUPPORTED_BROWSER] [{}] [{}] [{}]\", requestId, user, remoteHost, browser);\n            replyWithError(format(\"Cannot find %s capabilities on any available node\",\n                    caps.describe()), response);\n            return;\n        }\n\n        caps.setVersion(actualVersion.getNumber());\n\n        capabilityProcessorFactory.getProcessor(caps).process(caps);\n\n        List<Region> allRegions = actualVersion.getRegions()\n                .stream().map(Region::copy).collect(toList());\n        List<Region> unvisitedRegions = new ArrayList<>(allRegions);\n\n        int attempt = 0;\n        JsonMessage hubMessage = null;\n        try (CloseableHttpClient client = newHttpClient(routeTimeout * 1000)) {\n            if (actualVersion.getPermittedCount() != null) {\n                avblBrowsersChecker.ensureFreeBrowsersAvailable(user, remoteHost, browser, actualVersion);\n            }\n\n            while (!allRegions.isEmpty() && !terminated.get()) {\n                attempt++;\n\n                Region currentRegion = hostSelectionStrategy.selectRegion(allRegions, unvisitedRegions);\n                Host host = hostSelectionStrategy.selectHost(currentRegion.getHosts());\n\n                String route = host.getRoute();\n                try {\n                    LOGGER.info(\"[{}] [SESSION_ATTEMPTED] [{}] [{}] [{}] [{}] [{}]\", requestId, user, remoteHost, browser, route, attempt);\n\n                    String target = route + request.getRequestURI();\n                    HttpResponse hubResponse = client.execute(post(target, message));\n                    hubMessage = JsonMessageFactory.from(hubResponse.getEntity().getContent());\n\n                    if (hubResponse.getStatusLine().getStatusCode() == SC_OK) {\n                        String sessionId = hubMessage.getSessionId();\n                        hubMessage.setSessionId(host.getRouteId() + sessionId);\n                        replyWithOk(hubMessage, response);\n                        long createdDurationSeconds = Instant.now().getEpochSecond() - initialSeconds;\n                        LOGGER.info(\"[{}] [{}] [SESSION_CREATED] [{}] [{}] [{}] [{}] [{}] [{}]\",\n                                requestId, createdDurationSeconds, user, remoteHost, browser, route, sessionId, attempt);\n                        statsCounter.startSession(hubMessage.getSessionId(), user, caps.getBrowserName(), actualVersion.getNumber(), route);\n                        return;\n                    }\n                    LOGGER.warn(\"[{}] [SESSION_FAILED] [{}] [{}] [{}] [{}] - {}\",\n                            requestId, user, remoteHost, browser, route, hubMessage.getErrorMessage());\n                } catch (JsonProcessingException exception) {\n                    LOGGER.error(\"[{}] [BAD_HUB_JSON] [{}] [{}] [{}] [{}] - {}\", \"\",\n                            requestId, user, remoteHost, browser, route, exception.getMessage());\n                } catch (IOException exception) {\n                    LOGGER.error(\"[{}] [HUB_COMMUNICATION_FAILURE] [{}] [{}] [{}] - {}\",\n                            requestId, user, remoteHost, browser, route, exception.getMessage());\n                }\n\n                currentRegion.getHosts().remove(host);\n                if (currentRegion.getHosts().isEmpty()) {\n                    allRegions.remove(currentRegion);\n                }\n\n                unvisitedRegions.remove(currentRegion);\n                if (unvisitedRegions.isEmpty()) {\n                    unvisitedRegions = new ArrayList<>(allRegions);\n                }\n            }\n        } catch (AvailableBrowserCheckExeption e) {\n            LOGGER.error(\"[{}] [AVAILABLE_BROWSER_CHECK_ERROR] [{}] [{}] [{}] - {}\",\n                    requestId, user, remoteHost, browser, e.getMessage());\n        }\n\n        LOGGER.error(\"[{}] [SESSION_NOT_CREATED] [{}] [{}] [{}]\", requestId, user, remoteHost, browser);\n        if (hubMessage == null) {\n            replyWithError(\"Cannot create session on any available node\", response);\n        } else {\n            replyWithError(hubMessage, response);\n        }\n    }\n\n\n    protected void replyWithOk(JsonMessage message, HttpServletResponse response) throws IOException {\n        reply(SC_OK, message, response);\n    }\n\n    protected void replyWithError(String errorMessage, HttpServletResponse response) throws IOException {\n        replyWithError(JsonMessageFactory.error(13, errorMessage), response);\n    }\n\n    protected void replyWithError(JsonMessage message, HttpServletResponse response) throws IOException {\n        reply(SC_INTERNAL_SERVER_ERROR, message, response);\n    }\n\n    protected void reply(int code, JsonMessage message, HttpServletResponse response) throws IOException {\n        response.setStatus(code);\n        response.setContentType(APPLICATION_JSON.toString());\n        String messageRaw = message.toJson();\n        response.setContentLength(messageRaw.getBytes(UTF_8).length);\n        try (OutputStream output = response.getOutputStream()) {\n            IOUtils.write(messageRaw, output, UTF_8);\n        }\n    }\n\n    protected HttpPost post(String target, JsonMessage message) throws IOException {\n        HttpPost method = new HttpPost(target);\n        StringEntity entity = new StringEntity(message.toJson(), APPLICATION_JSON);\n        method.setEntity(entity);\n        method.setHeader(ACCEPT, APPLICATION_JSON.getMimeType());\n        return method;\n    }\n\n    protected CloseableHttpClient newHttpClient(int maxTimeout) {\n        return HttpClientBuilder.create().setDefaultRequestConfig(\n                RequestConfig.custom()\n                        .setConnectionRequestTimeout(10000)\n                        .setConnectTimeout(10000)\n                        .setSocketTimeout(maxTimeout)\n                        .build()\n        ).setRedirectStrategy(new LaxRedirectStrategy()).disableAutomaticRetries().build();\n    }\n}"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/SessionStorageEvictionScheduler.java",
    "content": "package ru.qatools.gridrouter;\n\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.scheduling.annotation.EnableScheduling;\nimport org.springframework.scheduling.annotation.Scheduled;\nimport ru.qatools.gridrouter.sessions.StatsCounter;\n\nimport java.time.Duration;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\n@Configuration\n@EnableScheduling\npublic class SessionStorageEvictionScheduler {\n\n    @Value(\"${grid.router.evict.sessions.timeout.seconds}\")\n    private int timeout;\n\n    @Autowired\n    private StatsCounter statsCounter;\n\n    @Scheduled(cron = \"${grid.router.evict.sessions.cron}\")\n    public void expireOldSessions() {\n        statsCounter.expireSessionsOlderThan(Duration.ofSeconds(timeout));\n    }\n}\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/SpringHttpServlet.java",
    "content": "package ru.qatools.gridrouter;\n\nimport javax.servlet.ServletConfig;\nimport javax.servlet.ServletException;\nimport javax.servlet.http.HttpServlet;\n\nimport static org.springframework.web.context.support.SpringBeanAutowiringSupport.processInjectionBasedOnServletContext;\n\n/**\n * @author Ilya Sadykov\n */\npublic abstract class SpringHttpServlet extends HttpServlet {\n    @Override\n    public void init(ServletConfig config) throws ServletException {\n        super.init(config);\n        processInjectionBasedOnServletContext(this, config.getServletContext());\n    }\n}\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/StatsServlet.java",
    "content": "package ru.qatools.gridrouter;\n\nimport org.apache.commons.io.IOUtils;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport ru.qatools.gridrouter.json.JsonFormatter;\nimport ru.qatools.gridrouter.sessions.StatsCounter;\n\nimport javax.servlet.ServletException;\nimport javax.servlet.annotation.HttpConstraint;\nimport javax.servlet.annotation.ServletSecurity;\nimport javax.servlet.annotation.WebServlet;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\nimport java.io.OutputStream;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static javax.servlet.http.HttpServletResponse.SC_OK;\nimport static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;\n\n/**\n * @author Dmitry Baev charlie@yandex-team.ru\n */\n@WebServlet(urlPatterns = {\"/stats\"}, asyncSupported = true)\n@ServletSecurity(value = @HttpConstraint(rolesAllowed = {\"user\"}))\npublic class StatsServlet extends SpringHttpServlet {\n\n    @Autowired\n    private transient StatsCounter statsCounter;\n\n    @Override\n    protected void doGet(HttpServletRequest request, HttpServletResponse response)\n            throws ServletException, IOException {\n        response.setStatus(SC_OK);\n        response.setContentType(APPLICATION_JSON_VALUE);\n        try (OutputStream output = response.getOutputStream()) {\n            IOUtils.write(JsonFormatter.toJson(\n                    statsCounter.getStats(request.getRemoteUser())\n            ), output, UTF_8);\n        }\n    }\n}\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/caps/AppiumCapabilityProcessor.java",
    "content": "package ru.qatools.gridrouter.caps;\n\nimport org.springframework.stereotype.Service;\nimport ru.qatools.gridrouter.json.JsonCapabilities;\n\nimport java.util.Map;\n\n/**\n * <p>\n * Sets \"keepKeyChains\" capability for Mac sessions.\n * </p>\n *\n * @author Ivan Krutov vania-pooh@yandex-team.ru\n * \n */\n@SuppressWarnings(\"JavadocReference\")\n@Service\npublic class AppiumCapabilityProcessor implements CapabilityProcessor {\n\n    private static final String PLATFORM_NAME = \"platformName\";\n    private static final String IOS = \"iOS\";\n    \n    @Override\n    public boolean accept(JsonCapabilities caps) {\n        return caps.getBrowserName().isEmpty() && isMac(caps);\n    }\n\n    @Override\n    public void process(JsonCapabilities caps) {\n        caps.any().put(\"keepKeyChains\", true);\n    }\n    \n    private boolean isMac(JsonCapabilities jsonCapabilities) {\n        Map<String, Object> capsMap = jsonCapabilities.any();\n        return \n                capsMap.containsKey(PLATFORM_NAME) &&\n                String.valueOf(capsMap.get(PLATFORM_NAME)).contains(IOS);\n                \n    }\n}\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/caps/CapabilityProcessor.java",
    "content": "package ru.qatools.gridrouter.caps;\n\nimport ru.qatools.gridrouter.json.JsonCapabilities;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic interface CapabilityProcessor {\n\n    boolean accept(JsonCapabilities caps);\n\n    void process(JsonCapabilities caps);\n}\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/caps/CapabilityProcessorFactory.java",
    "content": "package ru.qatools.gridrouter.caps;\n\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.stereotype.Component;\nimport ru.qatools.gridrouter.json.JsonCapabilities;\n\nimport java.util.List;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\n@Component\npublic class CapabilityProcessorFactory {\n\n    @Autowired\n    @SuppressWarnings(\"MismatchedQueryAndUpdateOfCollection\")\n    private List<CapabilityProcessor> processors;\n\n    public CapabilityProcessor getProcessor(JsonCapabilities caps) {\n        return processors.stream()\n                .filter(p -> p.accept(caps))\n                .findFirst()\n                .orElse(new DummyCapabilityProcessor());\n    }\n}\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/caps/DummyCapabilityProcessor.java",
    "content": "package ru.qatools.gridrouter.caps;\n\nimport ru.qatools.gridrouter.json.JsonCapabilities;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic class DummyCapabilityProcessor implements CapabilityProcessor {\n\n    @Override\n    public boolean accept(JsonCapabilities caps) {\n        throw new UnsupportedOperationException(\"Method DummyCapabilityProcessor::accept should never be called\");\n    }\n\n    @Override\n    public void process(JsonCapabilities caps) {\n    }\n}\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/caps/IECapabilityProcessor.java",
    "content": "package ru.qatools.gridrouter.caps;\n\nimport org.springframework.stereotype.Service;\nimport ru.qatools.gridrouter.json.JsonCapabilities;\nimport ru.qatools.gridrouter.json.Proxy;\n\n/**\n * <p>\n * Sets \"ie.ensureCleanSession\" and \"ie.usePerProcessProxy\" for all new\n * internet explorer sessions to ensure clean browser state.\n * </p>\n * <p>\n * Apart from that it sets the \"proxy\" capability to\n * {@link org.openqa.selenium.Proxy.ProxyType#DIRECT ProxyType.DIRECT}\n * because explorers tend to reuse the proxy from the previous sessions.\n * </p>\n *\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\n@SuppressWarnings(\"JavadocReference\")\n@Service\npublic class IECapabilityProcessor implements CapabilityProcessor {\n\n    private static final String IE_BROWSER_NAME = \"internet explorer\";\n\n    @Override\n    public boolean accept(JsonCapabilities caps) {\n        return caps.getBrowserName().equals(IE_BROWSER_NAME);\n    }\n\n    @Override\n    public void process(JsonCapabilities caps) {\n        caps.any().put(\"ie.ensureCleanSession\", true);\n        caps.any().put(\"ie.usePerProcessProxy\", true);\n        if (!caps.any().containsKey(\"proxy\")) {\n            Proxy proxy = new Proxy();\n            proxy.setProxyType(\"DIRECT\");\n            caps.any().put(\"proxy\", proxy);\n        }\n    }\n}\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/json/Describable.java",
    "content": "package ru.qatools.gridrouter.json;\n\nimport static org.apache.commons.lang3.StringUtils.isEmpty;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n * @author Dmitry Baev charlie@yandex-team.ru\n */\npublic interface Describable {\n\n    String getBrowserName();\n    String getVersion();\n\n    default String describe() {\n        return getBrowserName() + (isEmpty(getVersion()) ? \"\" : \"-\" + getVersion());\n    }\n}\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/json/JsonFormatter.java",
    "content": "package ru.qatools.gridrouter.json;\n\nimport com.fasterxml.jackson.annotation.JsonInclude;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.databind.ObjectMapper;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic class JsonFormatter {\n\n    public static String toJson(Object o) throws JsonProcessingException {\n        ObjectMapper mapper = new ObjectMapper();\n        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);\n        return mapper.writeValueAsString(o);\n    }\n}\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/json/JsonMessageFactory.java",
    "content": "package ru.qatools.gridrouter.json;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\n\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/**\n * @author Dmitry Baev charlie@yandex-team.ru\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic final class JsonMessageFactory {\n\n    JsonMessageFactory() {\n    }\n\n    public static JsonMessage from(String content) throws IOException {\n        return new ObjectMapper().readValue(content, JsonMessage.class);\n    }\n\n    public static JsonMessage from(InputStream stream) throws IOException {\n        return new ObjectMapper().readValue(stream, JsonMessage.class);\n    }\n\n    public static JsonMessage error(int status, String errorMessage) {\n        JsonMessage message = new JsonMessage();\n        message.setStatus(status);\n        message.setErrorMessage(errorMessage);\n        return message;\n    }\n}\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/json/JsonWithAnyProperties.java",
    "content": "package ru.qatools.gridrouter.json;\n\nimport com.fasterxml.jackson.annotation.JsonAnyGetter;\nimport com.fasterxml.jackson.annotation.JsonAnySetter;\n\nimport java.util.HashMap;\nimport java.util.Map;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic abstract class JsonWithAnyProperties {\n\n    private Map<String, Object> otherProperties = new HashMap<>();\n\n    @JsonAnyGetter\n    public Map<String, Object> any() {\n        return otherProperties;\n    }\n\n    @JsonAnySetter\n    public void set(String name, Object value) {\n        otherProperties.put(name, value);\n    }\n}\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/json/WithErrorMessage.java",
    "content": "package ru.qatools.gridrouter.json;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\n\nimport java.io.IOException;\nimport java.util.Map;\n\nimport static java.util.Collections.emptyMap;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic interface WithErrorMessage {\n\n    String VALUE_KEY = \"value\";\n    String MESSAGE_KEY = \"message\";\n\n    String DEFAULT_ERROR_MESSAGE = \"no error message was provided from hub\";\n\n    Map<String, Object> any();\n\n    void set(String name, Object value);\n\n    @JsonIgnore\n    @SuppressWarnings(\"unchecked\")\n    default String getErrorMessage() throws IOException {\n        try {\n            return (String) ((Map<String, Object>)\n                    any().getOrDefault(VALUE_KEY, emptyMap()))\n                         .getOrDefault(MESSAGE_KEY, DEFAULT_ERROR_MESSAGE);\n        } catch (ClassCastException ignored) {\n            return DEFAULT_ERROR_MESSAGE;\n        }\n    }\n\n    @JsonIgnore\n    default void setErrorMessage(String message) {\n        JsonValue value = new JsonValue();\n        value.setMessage(message);\n        set(VALUE_KEY, value);\n    }\n}\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/json/WithJsonView.java",
    "content": "package ru.qatools.gridrouter.json;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\n\n/**\n * @author Dmitry Baev charlie@yandex-team.ru\n */\npublic interface WithJsonView {\n\n    default String toJson() throws JsonProcessingException {\n        return JsonFormatter.toJson(this);\n    }\n}\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/sessions/AvailableBrowserCheckExeption.java",
    "content": "package ru.qatools.gridrouter.sessions;\n\n/**\n * @author Ilya Sadykov\n */\npublic class AvailableBrowserCheckExeption extends RuntimeException {\n    public AvailableBrowserCheckExeption(String message) {\n        super(message);\n    }\n\n    public AvailableBrowserCheckExeption(String message, Throwable cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/sessions/AvailableBrowsersChecker.java",
    "content": "package ru.qatools.gridrouter.sessions;\n\nimport ru.qatools.gridrouter.config.Version;\n\n/**\n * @author Ilya Sadykov\n */\npublic interface AvailableBrowsersChecker {\n    /**\n     * Blocks or throws an exception if there is no browsers available for user\n     */\n    void ensureFreeBrowsersAvailable(String user, String remoteHost, String browser, Version actualVersion);\n}\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/sessions/BrowserVersion.java",
    "content": "package ru.qatools.gridrouter.sessions;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic class BrowserVersion {\n\n    private final String browser;\n    private final String version;\n\n    public BrowserVersion(String browser, String version) {\n        this.browser = browser;\n        this.version = version;\n    }\n\n    public String getBrowser() {\n        return browser;\n    }\n\n    public String getVersion() {\n        return version;\n    }\n}\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/sessions/BrowsersCountMap.java",
    "content": "package ru.qatools.gridrouter.sessions;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Optional;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic class BrowsersCountMap extends HashMap<String, Map<String, Integer>> implements GridRouterUserStats {\n\n    public void increment(String browser, String version) {\n        putIfAbsent(browser, new HashMap<>());\n        get(browser).compute(version, (v, count) -> Optional.ofNullable(count).orElse(0) + 1);\n    }\n\n    public void decrement(BrowserVersion browser) {\n        decrement(browser.getBrowser(), browser.getVersion());\n    }\n\n    public void decrement(String browser, String version) {\n        if (!containsKey(browser)) {\n            return;\n        }\n\n        Map<String, Integer> versions = get(browser);\n        if (!versions.containsKey(version)) {\n            return;\n        }\n\n        int count = versions.get(version) - 1;\n        if (count > 0) {\n            versions.put(version, count);\n        } else {\n            versions.remove(version);\n        }\n\n        if (versions.isEmpty()) {\n            remove(browser);\n        }\n    }\n}\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/sessions/GridRouterUserStats.java",
    "content": "package ru.qatools.gridrouter.sessions;\n\nimport java.io.Serializable;\n\n/**\n * @author Ilya Sadykov\n */\npublic interface GridRouterUserStats extends Serializable {\n}\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/sessions/MemoryStatsCounter.java",
    "content": "package ru.qatools.gridrouter.sessions;\n\nimport java.time.Duration;\nimport java.time.temporal.Temporal;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\nimport static java.time.ZonedDateTime.now;\nimport static java.util.stream.Collectors.toList;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic class MemoryStatsCounter implements StatsCounter {\n\n    private final Map<String, Temporal> session2instant = new HashMap<>();\n    private final Map<String, String> session2user = new HashMap<>();\n    private final Map<String, BrowserVersion> session2browserVersion = new HashMap<>();\n    private final Map<String, BrowsersCountMap> user2browserCount = new HashMap<>();\n\n    @Override\n    public synchronized void startSession(String sessionId, String user, String browser, String version, String route) {\n        if (session2instant.put(sessionId, now()) == null) {\n            session2user.put(sessionId, user);\n            session2browserVersion.put(sessionId, new BrowserVersion(browser, version));\n            user2browserCount.putIfAbsent(user, new BrowsersCountMap());\n            user2browserCount.get(user).increment(browser, version);\n        }\n    }\n\n    @Override\n    public void updateSession(String sessionId, String route) {\n        session2instant.replace(sessionId, now());\n    }\n\n    @Override\n    public synchronized void deleteSession(String sessionId, String route) {\n        if (session2instant.remove(sessionId) != null) {\n            String user = session2user.remove(sessionId);\n            BrowserVersion browser = session2browserVersion.remove(sessionId);\n            user2browserCount.get(user).decrement(browser);\n        }\n    }\n\n    @Override\n    public void expireSessionsOlderThan(Duration duration) {\n        List<String> sessions2delete = session2instant.entrySet().stream()\n                .filter(e -> duration.compareTo(Duration.between(e.getValue(), now())) < 0)\n                .map(Map.Entry::getKey)\n                .collect(toList());\n        sessions2delete.stream().forEach(this::deleteSession);\n    }\n\n    @Override\n    public Set<String> getActiveSessions() {\n        return session2instant.keySet();\n    }\n\n    @Override\n    public synchronized BrowsersCountMap getStats(String user) {\n        return user2browserCount.getOrDefault(user, new BrowsersCountMap());\n    }\n\n    @Override\n    public int getSessionsCountForUser(String user) {\n        return user2browserCount.getOrDefault(user, new BrowsersCountMap()).values()\n                .parallelStream().flatMapToInt(version -> version.values().stream().mapToInt(Integer::intValue))\n                .sum();\n    }\n\n    @Override\n    public int getSessionsCountForUserAndBrowser(String user, String browser, String version) {\n        return user2browserCount.getOrDefault(user, new BrowsersCountMap()).entrySet()\n                .parallelStream().filter(entry -> entry.getKey().equals(browser))\n                .flatMapToInt(entry -> entry.getValue().entrySet().parallelStream()\n                        .filter(ver -> ver.getKey().equals(version)).mapToInt(Map.Entry::getValue)\n                ).sum();\n    }\n}\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/sessions/SkipAvailableBrowsersChecker.java",
    "content": "package ru.qatools.gridrouter.sessions;\n\nimport ru.qatools.gridrouter.config.Version;\n\n/**\n * @author Ilya Sadykov\n */\npublic class SkipAvailableBrowsersChecker implements AvailableBrowsersChecker {\n    @Override\n    public void ensureFreeBrowsersAvailable(String user, String remoteHost, String browser, Version actualVersion) {\n        // do nothing\n    }\n}\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/sessions/StatsCounter.java",
    "content": "package ru.qatools.gridrouter.sessions;\n\nimport java.time.Duration;\nimport java.util.Set;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic interface StatsCounter {\n\n    default void startSession(String sessionId, String user, String browser, String version) {\n        startSession(sessionId, user, browser, version, null);\n    }\n\n    default void updateSession(String sessionId) {\n        updateSession(sessionId, null);\n    }\n\n    default void deleteSession(String sessionId) {\n        deleteSession(sessionId, null);\n    }\n\n    void startSession(String sessionId, String user, String browser, String version, String route);\n\n    default void updateSession(String sessionId, String route) {\n\n    }\n\n    void deleteSession(String sessionId, String route);\n\n    void expireSessionsOlderThan(Duration duration);\n\n    Set<String> getActiveSessions();\n\n    GridRouterUserStats getStats(String user);\n\n    int getSessionsCountForUser(String user);\n\n    int getSessionsCountForUserAndBrowser(String user, String browser, String version);\n}\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/sessions/WaitAvailableBrowserTimeoutException.java",
    "content": "package ru.qatools.gridrouter.sessions;\n\n/**\n * @author Ilya Sadykov\n */\npublic class WaitAvailableBrowserTimeoutException extends AvailableBrowserCheckExeption {\n    public WaitAvailableBrowserTimeoutException(String message) {\n        super(message);\n    }\n\n    public WaitAvailableBrowserTimeoutException(String message, Throwable cause) {\n        super(message, cause);\n    }\n}\n"
  },
  {
    "path": "proxy/src/main/java/ru/qatools/gridrouter/sessions/WaitAvailableBrowsersChecker.java",
    "content": "package ru.qatools.gridrouter.sessions;\n\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.beans.factory.annotation.Value;\nimport ru.qatools.gridrouter.config.Version;\n\nimport java.time.Duration;\nimport java.time.temporal.Temporal;\n\nimport static java.lang.String.format;\nimport static java.time.ZonedDateTime.now;\nimport static java.util.UUID.randomUUID;\nimport static java.util.concurrent.TimeUnit.SECONDS;\n\n/**\n * @author Ilya Sadykov\n */\npublic class WaitAvailableBrowsersChecker implements AvailableBrowsersChecker {\n    private static final Logger LOGGER = LoggerFactory.getLogger(WaitAvailableBrowsersChecker.class);\n\n    @Value(\"${grid.router.queue.interval.seconds}\")\n    protected int queueWaitInterval;\n\n    @Autowired\n    protected StatsCounter statsCounter;\n\n    @Value(\"${grid.router.queue.timeout.seconds}\")\n    protected int queueTimeout;\n\n    public WaitAvailableBrowsersChecker() {\n    }\n\n    public WaitAvailableBrowsersChecker(int queueTimeout, int queueWaitInterval, StatsCounter statsCounter) {\n        this.queueTimeout = queueTimeout;\n        this.queueWaitInterval = queueWaitInterval;\n        this.statsCounter = statsCounter;\n    }\n\n    @Override\n    public void ensureFreeBrowsersAvailable(String user, String remoteHost, String browser, Version version) {\n        int waitAttempt = 0;\n        final String requestId = randomUUID().toString();\n        final Temporal waitingStarted = now();\n        final Duration maxWait = Duration.ofSeconds(queueTimeout);\n        while (maxWait.compareTo(Duration.between(waitingStarted, now())) > 0 &&\n                (countSessions(user, browser, version)) >= version.getPermittedCount()) {\n            try {\n                onWait(user, browser, version, requestId, waitAttempt);\n                Thread.sleep(SECONDS.toMillis(queueWaitInterval));\n            } catch (InterruptedException e) {\n                LOGGER.error(\"Failed to sleep thread\", e);\n            }\n            if (maxWait.compareTo(Duration.between(waitingStarted, now())) < 0) {\n                onWaitTimeout(user, browser, version, requestId, waitAttempt);\n            }\n        }\n        onWaitFinished(user, browser, version, requestId, waitAttempt);\n    }\n\n    protected void onWaitTimeout(String user, String browser, Version version, String requestId, int waitAttempt) {\n        throw new WaitAvailableBrowserTimeoutException(\n                format(\"Waiting for available browser of %s %s timed out for %s after %s attempts\",\n                        browser, version.getNumber(), user, waitAttempt));\n    }\n\n    protected void onWait(String user, String browser, Version version, String requestId, int waitAttempt) {\n        LOGGER.info(\"[SESSION_WAIT_AVAILABLE_BROWSER] [{}] [{}] [{}] [{}] [{}]\",\n                user, browser, version.getNumber(), version.getPermittedCount(), ++waitAttempt);\n    }\n\n    protected void onWaitFinished(String user, String browser, Version version, String requestId, int waitAttempt) {\n        LOGGER.info(\"[SESSION_WAIT_FINISHED] [{}] [{}] [{}] [{}] [{}]\",\n                user, browser, version.getNumber(), version.getPermittedCount(), ++waitAttempt);\n    }\n\n    protected int countSessions(String user, String browser, Version actualVersion) {\n        return statsCounter.getSessionsCountForUserAndBrowser(user, browser, actualVersion.getNumber());\n    }\n}\n"
  },
  {
    "path": "proxy/src/main/resources/META-INF/spring/application-context.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<beans xmlns=\"http://www.springframework.org/schema/beans\"\n       xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n       xmlns:context=\"http://www.springframework.org/schema/context\"\n       xsi:schemaLocation=\"\n            http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd\n            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd\">\n\n    <context:component-scan base-package=\"ru.qatools.gridrouter\"/>\n\n    <bean id=\"configurationProperties\"\n          class=\"org.springframework.beans.factory.config.PropertyPlaceholderConfigurer\">\n        <property name=\"locations\">\n            <list>\n                <value>classpath:application.properties</value>\n            </list>\n        </property>\n    </bean>\n\n    <bean id=\"configRepository\" class=\"${grid.router.quota.repository}\"/>\n    <bean id=\"hostSelectionStrategy\" class=\"${grid.router.host.selection.strategy}\"/>\n    <bean class=\"${grid.router.stats.counter}\"/>\n    <bean class=\"${grid.router.available.browsers.checker}\"/>\n\n</beans>\n"
  },
  {
    "path": "proxy/src/main/resources/application.properties",
    "content": "grid.config.quota.directory=classpath:quota\ngrid.router.quota.repository=ru.qatools.gridrouter.ConfigRepositoryXml\ngrid.config.quota.hotReload=true\ngrid.router.evict.sessions.cron=0 * * * * *\ngrid.router.evict.sessions.timeout.seconds=120\ngrid.router.route.timeout.seconds=120\ngrid.router.queue.timeout.seconds=120\ngrid.router.queue.interval.seconds=5\n\ngrid.router.host.selection.strategy=ru.qatools.gridrouter.config.RandomHostSelectionStrategy\ngrid.router.stats.counter=ru.qatools.gridrouter.sessions.MemoryStatsCounter\ngrid.router.available.browsers.checker=ru.qatools.gridrouter.sessions.SkipAvailableBrowsersChecker\n"
  },
  {
    "path": "proxy/src/main/resources/log4j.properties",
    "content": "# suppress inspection \"UnusedProperty\" for whole file\nlog4j.rootLogger=INFO, out\n\n# CONSOLE appender not used by default\nlog4j.appender.out=org.apache.log4j.ConsoleAppender\nlog4j.appender.out.layout=org.apache.log4j.PatternLayout\nlog4j.appender.out.layout.ConversionPattern=%d [%-10.10t] %-5p %-20.20c{1} - %m%n\nlog4j.throwableRenderer=org.apache.log4j.EnhancedThrowableRenderer\n"
  },
  {
    "path": "proxy/src/main/resources/xsd/json.xjb",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<jaxb:bindings\n        xmlns:jaxb=\"http://java.sun.com/xml/ns/jaxb\"\n        xmlns:xs=\"http://www.w3.org/2001/XMLSchema\"\n        xmlns:xjc=\"http://java.sun.com/xml/ns/jaxb/xjc\"\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xsi:schemaLocation=\"http://java.sun.com/xml/ns/jaxb http://java.sun.com/xml/ns/jaxb/bindingschema_2_1.xsd\"\n        xmlns:inheritance=\"http://jaxb2-commons.dev.java.net/basic/inheritance\"\n        jaxb:extensionBindingPrefixes=\"xjc annox\"\n        schemaLocation=\"json.xsd\"\n        version=\"2.1\">\n\n    <jaxb:bindings node=\"//xs:complexType[starts-with(@name, 'Json')]\" multiple=\"true\">\n        <inheritance:extends>ru.qatools.gridrouter.json.JsonWithAnyProperties</inheritance:extends>\n    </jaxb:bindings>\n\n    <jaxb:bindings node=\"//xs:complexType[@name='JsonMessage']\">\n        <inheritance:implements>ru.qatools.gridrouter.json.WithJsonView</inheritance:implements>\n        <inheritance:implements>ru.qatools.gridrouter.json.WithErrorMessage</inheritance:implements>\n    </jaxb:bindings>\n\n    <jaxb:bindings node=\"//xs:complexType[@name='JsonCapabilities']\">\n        <inheritance:implements>ru.qatools.gridrouter.json.Describable</inheritance:implements>\n    </jaxb:bindings>\n\n    <jaxb:globalBindings>\n        <jaxb:serializable/>\n        <xjc:simple/>\n    </jaxb:globalBindings>\n\n</jaxb:bindings>\n"
  },
  {
    "path": "proxy/src/main/resources/xsd/json.xsd",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<xsd:schema attributeFormDefault=\"unqualified\" elementFormDefault=\"unqualified\"\n            xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"\n            xmlns:json=\"urn:json.gridrouter.qatools.ru\"\n            targetNamespace=\"urn:json.gridrouter.qatools.ru\">\n\n    <xsd:element name=\"jsonMessage\" type=\"json:JsonMessage\"/>\n    <xsd:complexType name=\"JsonMessage\">\n        <xsd:sequence>\n            <xsd:element name=\"desiredCapabilities\" type=\"json:JsonCapabilities\"/>\n        </xsd:sequence>\n        <xsd:attribute name=\"status\" type=\"xsd:int\"/>\n        <xsd:attribute name=\"sessionId\" type=\"xsd:string\"/>\n    </xsd:complexType>\n\n    <xsd:complexType name=\"JsonCapabilities\">\n        <xsd:attribute name=\"browserName\" type=\"xsd:string\"/>\n        <xsd:attribute name=\"version\" type=\"xsd:string\"/>\n    </xsd:complexType>\n\n    <xsd:complexType name=\"JsonValue\">\n        <xsd:attribute name=\"message\" type=\"xsd:string\"/>\n    </xsd:complexType>\n\n    <xsd:complexType name=\"Proxy\">\n        <xsd:attribute name=\"proxyType\" type=\"xsd:string\"/>\n    </xsd:complexType>\n</xsd:schema>\n"
  },
  {
    "path": "proxy/src/main/webapp/WEB-INF/web.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<web-app\n        xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n        xmlns=\"http://java.sun.com/xml/ns/javaee\"\n        xsi:schemaLocation=\"http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd\"\n        metadata-complete=\"false\"\n        version=\"3.0\">\n\n    <context-param>\n        <param-name>contextConfigLocation</param-name>\n        <param-value>classpath:META-INF/spring/*application-context.xml</param-value>\n    </context-param>\n\n    <listener>\n        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>\n    </listener>\n\n    <servlet>\n        <servlet-name>default</servlet-name>\n        <servlet-class>org.eclipse.jetty.servlet.DefaultServlet</servlet-class>\n        <init-param>\n            <param-name>dirAllowed</param-name>\n            <param-value>false</param-value>\n        </init-param>\n    </servlet>\n\n    <login-config>\n        <auth-method>BASIC</auth-method>\n        <realm-name>Selenium Grid Router</realm-name>\n    </login-config>\n\n</web-app>\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/CommandDecodingTest.java",
    "content": "package ru.qatools.gridrouter;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.junit.runners.Parameterized;\n\nimport java.net.URLEncoder;\nimport java.util.Arrays;\nimport java.util.Collection;\n\nimport static java.nio.charset.StandardCharsets.UTF_8;\nimport static org.hamcrest.Matchers.endsWith;\nimport static org.junit.Assert.assertThat;\n\n/**\n * @author Artem Eroshenko eroshenkoam@yandex-team.ru\n */\n@RunWith(Parameterized.class)\npublic class CommandDecodingTest {\n\n    public static final String SUFFIX = \"http://host.com/wd/hub/session/8dec71ede39ad9ff3\";\n\n    public static final String POSTFIX = \"b3fbc03311bdc45282358f1-f09c-4c44-8057-4b82f4a53002/element/id/\";\n\n    public String requestUri;\n\n    public String elementId;\n\n    public CommandDecodingTest(String elementId) throws Exception {\n        this.requestUri = String.format(\"%s%s%s\", SUFFIX, POSTFIX, URLEncoder.encode(elementId, UTF_8.name()));\n        this.elementId = elementId;\n    }\n\n    @Parameterized.Parameters\n    public static Collection<Object[]> getData() {\n        return Arrays.asList(\n                new Object[]{\"text_???\"},\n                new Object[]{\"text_&_not_text\"}\n        );\n    }\n\n    @Test\n    public void testOutput() throws Exception {\n        assertThat(JsonWireUtils.getCommand(requestUri), endsWith(elementId));\n    }\n}\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/JsonWireUtilsTest.java",
    "content": "package ru.qatools.gridrouter;\n\nimport org.junit.Test;\n\nimport java.nio.charset.StandardCharsets;\n\nimport static java.util.UUID.randomUUID;\nimport static org.apache.commons.codec.digest.DigestUtils.md5Hex;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.is;\nimport static ru.qatools.gridrouter.JsonWireUtils.WD_HUB_SESSION;\nimport static ru.qatools.gridrouter.JsonWireUtils.getFullSessionId;\nimport static ru.qatools.gridrouter.JsonWireUtils.getSessionHash;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic class JsonWireUtilsTest {\n\n    @Test\n    public void testGetSessionHash() {\n        String routeHash = md5Hex(\"hubAddress\".getBytes(StandardCharsets.UTF_8));\n        assertThat(getSessionHash(sessionRequest(routeHash, randomUUID().toString(), \"\")), is(equalTo(routeHash)));\n        assertThat(getSessionHash(sessionRequest(routeHash, randomUUID().toString(), \"dhgdhg\")), is(equalTo(routeHash)));\n        assertThat(getSessionHash(sessionRequest(routeHash, randomUUID().toString(), \"dh/gdh/\")), is(equalTo(routeHash)));\n        assertThat(getSessionHash(sessionRequest(routeHash, randomUUID().toString(), \"dh/gdh/g\")), is(equalTo(routeHash)));\n    }\n\n    @Test\n    public void testGetFullSessionId() {\n        String routeHash = md5Hex(\"hubAddress\".getBytes(StandardCharsets.UTF_8));\n        String sessionId = randomUUID().toString();\n        String expected = routeHash + sessionId;\n        assertThat(getFullSessionId(sessionRequest(routeHash, sessionId, \"\")), is(equalTo(expected)));\n        assertThat(getFullSessionId(sessionRequest(routeHash, sessionId, \"sfgsds\")), is(equalTo(expected)));\n        assertThat(getFullSessionId(sessionRequest(routeHash, sessionId, \"sfg/sds/\")), is(equalTo(expected)));\n        assertThat(getFullSessionId(sessionRequest(routeHash, sessionId, \"sfg/sds/adfad\")), is(equalTo(expected)));\n    }\n\n    public String sessionRequest(String routeHash, String sessionId, String sessionCommand) {\n        if (!sessionCommand.isEmpty()) {\n            sessionCommand = \"/\".concat(sessionCommand);\n        }\n        return WD_HUB_SESSION + routeHash + sessionId + sessionCommand;\n    }\n}\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/PingServletTest.java",
    "content": "package ru.qatools.gridrouter;\n\nimport org.apache.http.client.methods.HttpGet;\nimport org.apache.http.impl.client.HttpClientBuilder;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport ru.qatools.gridrouter.utils.GridRouterRule;\n\nimport java.io.IOException;\n\nimport static javax.servlet.http.HttpServletResponse.SC_OK;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.equalTo;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic class PingServletTest {\n\n    @Rule\n    public GridRouterRule gridRouter = new GridRouterRule();\n\n    @Test\n    public void testPingWithAuth() throws IOException {\n        assertThat(executeSimpleGet(gridRouter.baseUrlWithAuth + \"/ping\"), equalTo(SC_OK));\n    }\n\n    public static int executeSimpleGet(String url) throws IOException {\n        return HttpClientBuilder\n                .create().build()\n                .execute(new HttpGet(url))\n                .getStatusLine()\n                .getStatusCode();\n    }\n}\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/ProxyServletExceptionsWithHubTest.java",
    "content": "package ru.qatools.gridrouter;\n\nimport org.junit.After;\nimport org.junit.Rule;\nimport ru.qatools.gridrouter.utils.HubEmulatorRule;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic class ProxyServletExceptionsWithHubTest extends ProxyServletExceptionsWithoutHubTest {\n\n    @Rule\n    public HubEmulatorRule hub = new HubEmulatorRule( 8081);\n\n    @After\n    public void tearDown() {\n        hub.verify().totalRequestsCountIs(0);\n    }\n}\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/ProxyServletExceptionsWithoutHubTest.java",
    "content": "package ru.qatools.gridrouter;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.openqa.selenium.UnsupportedCommandException;\nimport org.openqa.selenium.WebDriverException;\nimport org.openqa.selenium.remote.DesiredCapabilities;\nimport org.openqa.selenium.remote.RemoteWebDriver;\nimport ru.qatools.gridrouter.utils.GridRouterRule;\n\nimport static org.openqa.selenium.remote.DesiredCapabilities.chrome;\nimport static org.openqa.selenium.remote.DesiredCapabilities.firefox;\nimport static ru.qatools.gridrouter.utils.GridRouterRule.hubUrl;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic class ProxyServletExceptionsWithoutHubTest {\n\n    @Rule\n    public GridRouterRule gridRouter = new GridRouterRule();\n\n    @Test(expected = UnsupportedCommandException.class)\n    public void testProxyWithWrongAuth() {\n        new RemoteWebDriver(hubUrl(gridRouter.baseUrlWrongPassword), firefox());\n    }\n\n    @Test(expected = UnsupportedCommandException.class)\n    public void testProxyWithoutAuth() {\n        new RemoteWebDriver(hubUrl(gridRouter.baseUrl), firefox());\n    }\n\n    @Test(expected = WebDriverException.class)\n    public void testProxyWithNotSupportedBrowser() {\n        new RemoteWebDriver(hubUrl(gridRouter.baseUrlWithAuth), chrome());\n    }\n\n    @Test(expected = WebDriverException.class)\n    public void testProxyWithNotSupportedVersion() {\n        DesiredCapabilities caps = firefox();\n        caps.setVersion(\"1\");\n        new RemoteWebDriver(hubUrl(gridRouter.baseUrlWithAuth), caps);\n    }\n}\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/ProxyServletTest.java",
    "content": "package ru.qatools.gridrouter;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.openqa.selenium.By;\nimport org.openqa.selenium.WebDriverException;\nimport org.openqa.selenium.WebElement;\nimport org.openqa.selenium.remote.DesiredCapabilities;\nimport org.openqa.selenium.remote.RemoteWebDriver;\nimport org.openqa.selenium.remote.RemoteWebElement;\nimport ru.qatools.gridrouter.utils.GridRouterRule;\n\nimport java.net.URL;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.*;\nimport static org.openqa.selenium.Platform.ANY;\nimport static org.openqa.selenium.remote.DesiredCapabilities.firefox;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic abstract class ProxyServletTest {\n\n    @Rule\n    public GridRouterRule gridRouter = new GridRouterRule();\n\n    private final URL url;\n\n    public ProxyServletTest(String user) {\n        url = GridRouterRule.hubUrl(gridRouter.baseUrl(user));\n    }\n\n    protected final URL getUrl() {\n        return url;\n    }\n\n    @Test\n    public void testSpecifyingBrowserVersion() {\n        DesiredCapabilities caps = firefox();\n        caps.setVersion(\"32\");\n        new RemoteWebDriver(getUrl(), caps);\n    }\n\n    @Test\n    public void testSessionIdDoesNotChange() {\n        RemoteWebDriver driver = new RemoteWebDriver(getUrl(), firefox());\n        String sessionId = driver.getSessionId().toString();\n        driver.getCurrentUrl();\n        driver.get(\"some url\");\n        assertThat(driver.getSessionId().toString(), is(equalTo(sessionId)));\n        driver.getCurrentUrl();\n        assertThat(driver.getSessionId().toString(), is(equalTo(sessionId)));\n    }\n\n    @Test\n    public void testSessionIdChangesForANewBrowser() {\n        RemoteWebDriver driver1 = new RemoteWebDriver(getUrl(), firefox());\n        String sessionId1 = driver1.getSessionId().toString();\n        RemoteWebDriver driver2 = new RemoteWebDriver(getUrl(), firefox());\n        String sessionId2 = driver2.getSessionId().toString();\n        assertThat(sessionId1, is(not(equalTo(sessionId2))));\n    }\n\n    @Test\n    public void testQuit() {\n        RemoteWebDriver driver = new RemoteWebDriver(getUrl(), firefox());\n        driver.quit();\n    }\n\n    @Test\n    public void testSendRequestParams() {\n        RemoteWebDriver driver = new RemoteWebDriver(getUrl(), firefox());\n        String url = \"some url\";\n        driver.getCurrentUrl();\n        driver.get(url);\n        assertThat(driver.getCurrentUrl(), is(url));\n    }\n\n    @Test\n    public void testFindElement() {\n        RemoteWebDriver driver = new RemoteWebDriver(getUrl(), firefox());\n        driver.getCurrentUrl();\n        String selector = \"//lol[foo='bar']\";\n        WebElement element = driver.findElement(By.xpath(selector));\n        assertThat(\n                ((RemoteWebElement) element).getId(),\n                is(String.valueOf(selector.hashCode()))\n        );\n    }\n\n    @Test\n    public void testNullVersion() throws Exception {\n        String browserName = \"other\";\n        try {\n            new RemoteWebDriver(getUrl(), new DesiredCapabilities(browserName, null, ANY));\n        } catch (WebDriverException e) {\n            assertThat(e.getMessage(),\n                    startsWith(\"Cannot find \" + browserName + \" capabilities on any available node\"));\n        }\n    }\n}\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/ProxyServletWithBrokenAndOkHubsTest.java",
    "content": "package ru.qatools.gridrouter;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.openqa.selenium.remote.RemoteWebDriver;\nimport ru.qatools.gridrouter.utils.GridRouterRule;\nimport ru.qatools.gridrouter.utils.HubEmulatorRule;\n\nimport static org.openqa.selenium.remote.DesiredCapabilities.firefox;\nimport static ru.qatools.gridrouter.utils.GridRouterRule.USER_2;\nimport static ru.qatools.gridrouter.utils.GridRouterRule.hubUrl;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic class ProxyServletWithBrokenAndOkHubsTest {\n\n    @Rule\n    public GridRouterRule gridRouter = new GridRouterRule();\n\n    @Rule\n    public HubEmulatorRule hub1 = new HubEmulatorRule(8081, hub -> hub.emulate().newSessionFailures(1));\n\n    @Rule\n    public HubEmulatorRule hub2 = new HubEmulatorRule(8082, hub -> hub.emulate().newSessions(1));\n\n    @Test\n    public void testFailingHubIsSkipped() {\n        new RemoteWebDriver(hubUrl(gridRouter.baseUrl(USER_2)), firefox());\n        hub1.verify().totalRequestsCountIs(1);\n        hub1.verify().totalRequestsCountIs(1);\n    }\n}\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/ProxyServletWithBrokenHubTest.java",
    "content": "package ru.qatools.gridrouter;\n\nimport org.junit.ClassRule;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.openqa.selenium.WebDriverException;\nimport org.openqa.selenium.remote.RemoteWebDriver;\nimport ru.qatools.gridrouter.utils.GridRouterRule;\nimport ru.qatools.gridrouter.utils.HubEmulatorRule;\n\nimport static org.openqa.selenium.remote.DesiredCapabilities.firefox;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic class ProxyServletWithBrokenHubTest {\n\n    @ClassRule\n    public static GridRouterRule gridRouter = new GridRouterRule();\n\n    @Rule\n    public HubEmulatorRule hub = new HubEmulatorRule( 8081, hub -> hub.emulate().newSessionFailures(1));\n\n    @Test(expected = WebDriverException.class)\n    public void testFailingHubIsSkipped() {\n        new RemoteWebDriver(GridRouterRule.hubUrl(gridRouter.baseUrlWithAuth), firefox());\n        hub.verify().totalRequestsCountIs(1);\n    }\n}\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/ProxyServletWithOneHubTest.java",
    "content": "package ru.qatools.gridrouter;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.openqa.selenium.remote.RemoteWebDriver;\nimport ru.qatools.gridrouter.utils.HubEmulatorRule;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.openqa.selenium.remote.DesiredCapabilities.firefox;\nimport static ru.qatools.gridrouter.utils.GridRouterRule.USER_1;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic class ProxyServletWithOneHubTest extends ProxyServletTest {\n\n    @Rule\n    public HubEmulatorRule hub = new HubEmulatorRule( 8081,\n            hub -> hub.emulate().newSessions(1)\n    );\n\n    public ProxyServletWithOneHubTest() throws Exception {\n        super(USER_1);\n    }\n\n    @Test\n    public void testSessionIdsHaveACommonPrefix() {\n        hub.emulate().newSessions(1);\n\n        RemoteWebDriver driver1 = new RemoteWebDriver(getUrl(), firefox());\n        String sessionId1 = driver1.getSessionId().toString();\n        RemoteWebDriver driver2 = new RemoteWebDriver(getUrl(), firefox());\n        String sessionId2 = driver2.getSessionId().toString();\n        assertThat(\"sessionIds should have the same prefix\",\n                sessionId1.regionMatches(0, sessionId2, 0, 30));\n\n        hub.verify().totalRequestsCountIs(2);\n    }\n\n    @Test\n    @Override\n    public void testSpecifyingBrowserVersion() {\n        super.testSpecifyingBrowserVersion();\n        hub.verify().totalRequestsCountIs(1);\n    }\n\n    @Test\n    @Override\n    public void testSessionIdDoesNotChange() {\n        hub.emulate().navigation();\n        super.testSessionIdDoesNotChange();\n        hub.verify().totalRequestsCountIs(4);\n    }\n\n    @Test\n    @Override\n    public void testSessionIdChangesForANewBrowser() {\n        hub.emulate().newSessions(1);\n        super.testSessionIdChangesForANewBrowser();\n        hub.verify().totalRequestsCountIs(2);\n    }\n\n    @Test\n    @Override\n    public void testQuit() {\n        hub.emulate().quit();\n        super.testQuit();\n        hub.verify().newSessionRequestsCountIs(1)\n                .quitRequestsCountIs(1);\n    }\n\n    @Override\n    public void testSendRequestParams() {\n        hub.emulate().navigation();\n        super.testSendRequestParams();\n        hub.verify().totalRequestsCountIs(4);\n    }\n\n    @Override\n    public void testFindElement() {\n        hub.emulate().navigation().findElement();\n        super.testFindElement();\n        hub.verify().totalRequestsCountIs(3);\n    }\n}\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/ProxyServletWithTwoHubsTest.java",
    "content": "package ru.qatools.gridrouter;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.openqa.selenium.remote.RemoteWebDriver;\nimport ru.qatools.gridrouter.utils.HubEmulatorRule;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.openqa.selenium.remote.DesiredCapabilities.firefox;\nimport static ru.qatools.gridrouter.utils.GridRouterRule.USER_2;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic class ProxyServletWithTwoHubsTest extends ProxyServletTest {\n\n    @Rule\n    public HubEmulatorRule hub1 = new HubEmulatorRule( 8081, hub -> hub.emulate().newSessions(1));\n\n    @Rule\n    public HubEmulatorRule hub2 = new HubEmulatorRule( 8082, hub -> hub.emulate().newSessions(1));\n\n    public ProxyServletWithTwoHubsTest() throws Exception {\n        super(USER_2);\n    }\n\n    @Test\n    public void testSessionIdsHaveNoCommonPrefix() {\n        RemoteWebDriver driver1 = new RemoteWebDriver(getUrl(), firefox());\n        String sessionId1 = driver1.getSessionId().toString();\n        RemoteWebDriver driver2 = new RemoteWebDriver(getUrl(), firefox());\n        String sessionId2 = driver2.getSessionId().toString();\n        assertThat(\"sessionIds should not have the same prefix\",\n                !sessionId1.regionMatches(0, sessionId2, 0, 30));\n\n        hub1.verify().totalRequestsCountIs(1);\n        hub2.verify().totalRequestsCountIs(1);\n    }\n\n    @Override\n    public void testSpecifyingBrowserVersion() {\n        super.testSpecifyingBrowserVersion();\n    }\n\n    @Override\n    public void testSessionIdDoesNotChange() {\n        hub1.emulate().navigation();\n        hub2.emulate().navigation();\n        super.testSessionIdDoesNotChange();\n    }\n\n    @Test\n    @Override\n    public void testSessionIdChangesForANewBrowser() {\n        super.testSessionIdChangesForANewBrowser();\n        hub1.verify().totalRequestsCountIs(1);\n        hub2.verify().totalRequestsCountIs(1);\n    }\n\n    @Override\n    public void testQuit() {\n        hub1.emulate().quit();\n        hub2.emulate().quit();\n        super.testQuit();\n    }\n\n    @Override\n    public void testSendRequestParams() {\n        hub1.emulate().navigation();\n        hub2.emulate().navigation();\n        super.testSendRequestParams();\n    }\n\n    @Test\n    @Override\n    public void testFindElement() {\n        hub1.emulate().navigation().findElement();\n        hub2.emulate().navigation().findElement();\n        super.testFindElement();\n    }\n}\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/ProxyServletWithoutHubTest.java",
    "content": "package ru.qatools.gridrouter;\n\nimport org.junit.ClassRule;\nimport org.junit.Test;\nimport org.openqa.selenium.WebDriverException;\nimport org.openqa.selenium.remote.RemoteWebDriver;\nimport ru.qatools.gridrouter.utils.GridRouterRule;\n\nimport static org.openqa.selenium.remote.DesiredCapabilities.firefox;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic class ProxyServletWithoutHubTest {\n\n    @ClassRule\n    public static GridRouterRule gridRouterRule = new GridRouterRule();\n\n    @Test(expected = WebDriverException.class)\n    public void testProxyWithProperAuth() {\n        new RemoteWebDriver(GridRouterRule.hubUrl(gridRouterRule.baseUrlWithAuth), firefox());\n    }\n}\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/QuotaReloadTest.java",
    "content": "package ru.qatools.gridrouter;\n\nimport org.junit.*;\nimport ru.qatools.gridrouter.utils.GridRouterRule;\nimport ru.qatools.gridrouter.utils.HubEmulatorRule;\n\nimport static java.util.concurrent.TimeUnit.SECONDS;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.openqa.selenium.remote.DesiredCapabilities.firefox;\nimport static ru.qatools.gridrouter.utils.GridRouterRule.USER_1;\nimport static ru.qatools.gridrouter.utils.GridRouterRule.USER_4;\nimport static ru.qatools.gridrouter.utils.MatcherUtils.canObtain;\nimport static ru.qatools.gridrouter.utils.QuotaUtils.*;\nimport static ru.yandex.qatools.matchers.decorators.MatcherDecorators.should;\nimport static ru.yandex.qatools.matchers.decorators.MatcherDecorators.timeoutHasExpired;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\n@Ignore\npublic class QuotaReloadTest {\n\n    public static final int HUB_PORT_2 = 8082;\n    @Rule\n    public GridRouterRule gridRouter = new GridRouterRule();\n\n    @Rule\n    public HubEmulatorRule hub2 = new HubEmulatorRule( HUB_PORT_2, hub -> hub.emulate().newSessions(1));\n\n    @Test\n    public void testQuotaIsReloadedOnFileChange() throws Exception {\n        replacePortInQuotaFile(USER_1, hub2.getPort());\n        assertThat(USER_1, should(canObtain(gridRouter, firefox()))\n                .whileWaitingUntil(timeoutHasExpired(SECONDS.toMillis(60))\n                        .withPollingInterval(SECONDS.toMillis(3))));\n    }\n\n    @Test\n    public void testNewQuotaFileIsLoaded() throws Exception {\n        copyQuotaFile(USER_1, USER_4, 0, 0, hub2.getPort());\n        assertThat(USER_4, should(canObtain(gridRouter, firefox()))\n                .whileWaitingUntil(timeoutHasExpired(SECONDS.toMillis(60))\n                        .withPollingInterval(SECONDS.toMillis(3))));\n    }\n\n    @After\n    public void tearDown() {\n        hub2.verify().newSessionRequestsCountIs(1);\n        hub2.verify().totalRequestsCountIs(1);\n    }\n\n    @AfterClass\n    public static void restoreQuotaFiles() throws Exception {\n        replacePortInQuotaFile(USER_1, 8081);\n        deleteQuotaFile(USER_4);\n    }\n}\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/QuotaServletTest.java",
    "content": "package ru.qatools.gridrouter;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.apache.http.client.methods.CloseableHttpResponse;\nimport org.apache.http.client.methods.HttpGet;\nimport org.apache.http.impl.client.HttpClientBuilder;\nimport org.junit.ClassRule;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.junit.runners.Parameterized;\nimport org.junit.runners.Parameterized.Parameters;\nimport ru.qatools.gridrouter.utils.GridRouterRule;\n\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\nimport static ru.qatools.gridrouter.utils.GridRouterRule.*;\n\n/**\n * TODO add test for user with different browsers and different versions\n * @author Dmitry Baev charlie@yandex-team.ru\n */\n@RunWith(Parameterized.class)\npublic class QuotaServletTest {\n\n    @ClassRule\n    public static GridRouterRule gridRouter = new GridRouterRule();\n\n    @Parameters(name = \"{0}\")\n    public static Collection<Object[]> data() {\n        return Arrays.asList(new Object[][]{\n                {USER_1, 1}, {USER_2, 4}, {USER_3, 8},\n        });\n    }\n\n    private final String user;\n    private final int browsersCount;\n\n    public QuotaServletTest(String user, int browsersCount) {\n        this.user = user;\n        this.browsersCount = browsersCount;\n    }\n\n    @Test\n    public void testQuota() throws IOException {\n        Map<String, Integer> quota = executeSimpleGet(gridRouter.baseUrl(user) + \"/quota\");\n        assertThat(quota.size(), is(1));\n        assertThat(quota.get(\"firefox:32.0\"), is(browsersCount));\n    }\n\n    public static Map<String, Integer> executeSimpleGet(String url) throws IOException {\n        CloseableHttpResponse execute = HttpClientBuilder\n                .create().build()\n                .execute(new HttpGet(url));\n        InputStream content = execute.getEntity().getContent();\n        //noinspection unchecked\n        return new ObjectMapper().readValue(content, HashMap.class);\n\n    }\n}\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/RegionsTest.java",
    "content": "package ru.qatools.gridrouter;\n\nimport org.junit.ClassRule;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.openqa.selenium.WebDriverException;\nimport org.openqa.selenium.remote.RemoteWebDriver;\nimport ru.qatools.gridrouter.utils.GridRouterRule;\nimport ru.qatools.gridrouter.utils.HubEmulatorRule;\n\nimport static org.openqa.selenium.remote.DesiredCapabilities.firefox;\nimport static ru.qatools.gridrouter.utils.GridRouterRule.*;\n\n/**\n * @author Dmitry Baev charlie@yandex-team.ru\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic class RegionsTest {\n\n    @ClassRule\n    public static GridRouterRule gridRouter = new GridRouterRule();\n\n    @Rule\n    public HubEmulatorRule hub1 = new HubEmulatorRule( 8081);\n\n    @Rule\n    public HubEmulatorRule hub2 = new HubEmulatorRule( 8082);\n\n    @Rule\n    public HubEmulatorRule hub3 = new HubEmulatorRule( 8083);\n\n    @Test\n    public void testRegionIsChangedAfterFailedTry() {\n        hub3.emulate().newSessions(1);\n        new RemoteWebDriver(hubUrl(gridRouter.baseUrl(USER_3)), firefox());\n        hub1.verify().newSessionRequestsCountIs(1);\n        hub2.verify().newSessionRequestsCountIs(0);\n        hub3.verify().newSessionRequestsCountIs(1);\n    }\n\n    @Test\n    public void testAllHostsAreTriedExactlyOnceInTheEnd() {\n        getWebDriverSafe(USER_3);\n        hub1.verify().newSessionRequestsCountIs(1);\n        hub2.verify().newSessionRequestsCountIs(1);\n        hub3.verify().newSessionRequestsCountIs(1);\n    }\n\n    @Test\n    public void testConfigIsImmutableBetweenRequests() {\n        // note here user1 is used for simplicity\n        getWebDriverSafe(USER_1);\n        hub1.verify().newSessionRequestsCountIs(1);\n        getWebDriverSafe(USER_1);\n        hub1.verify().newSessionRequestsCountIs(2);\n    }\n\n    private static void getWebDriverSafe(String user) {\n        try {\n            new RemoteWebDriver(hubUrl(gridRouter.baseUrl(user)), firefox());\n        } catch (WebDriverException ignored) {\n        }\n    }\n}\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/RouteServletTest.java",
    "content": "package ru.qatools.gridrouter;\n\nimport org.junit.ClassRule;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.openqa.selenium.WebDriverException;\nimport org.openqa.selenium.remote.RemoteWebDriver;\nimport ru.qatools.gridrouter.utils.GridRouterRule;\nimport ru.qatools.gridrouter.utils.HubEmulatorRule;\n\nimport static org.openqa.selenium.remote.DesiredCapabilities.firefox;\nimport static ru.qatools.gridrouter.utils.GridRouterRule.USER_3;\nimport static ru.qatools.gridrouter.utils.GridRouterRule.hubUrl;\n\npublic class RouteServletTest {\n\n    @ClassRule\n    public static GridRouterRule gridRouter = new GridRouterRule();\n\n    @Rule\n    public HubEmulatorRule hub = new HubEmulatorRule( 8081);\n\n    @Test(expected = WebDriverException.class, timeout = 10 * 1000)\n    public void testRouteTimeout() {\n            hub.emulate().newSessionFreeze(30);\n            new RemoteWebDriver(hubUrl(gridRouter.baseUrl(USER_3)), firefox());\n    }\n\n}"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/StatsServletTest.java",
    "content": "package ru.qatools.gridrouter;\n\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.openqa.selenium.WebDriver;\nimport org.openqa.selenium.remote.RemoteWebDriver;\nimport ru.qatools.gridrouter.sessions.BrowsersCountMap;\nimport ru.qatools.gridrouter.utils.GridRouterRule;\nimport ru.qatools.gridrouter.utils.HubEmulatorRule;\n\nimport java.io.IOException;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\nimport static org.openqa.selenium.remote.DesiredCapabilities.firefox;\nimport static ru.qatools.gridrouter.utils.GridRouterRule.*;\nimport static ru.qatools.gridrouter.utils.HttpUtils.executeSimpleGet;\n\n/**\n * @author Dmitry Baev charlie@yandex-team.ru\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic class StatsServletTest {\n\n    @Rule\n    public GridRouterRule gridRouter = new GridRouterRule();\n\n    @Rule\n    public HubEmulatorRule hub = new HubEmulatorRule(8081);\n\n    @Test\n    public void testStats() throws IOException {\n        assertThat(getActual(USER_1), is(empty()));\n\n        hub.emulate().newSessions(1);\n        hub.emulate().quit();\n\n        WebDriver driver = new RemoteWebDriver(hubUrl(gridRouter.baseUrlWithAuth), firefox());\n        assertThat(getActual(USER_1), is(newCountMap(\"firefox\", \"32.0\")));\n\n        driver.quit();\n        assertThat(getActual(USER_1), is(empty()));\n    }\n\n    @Test\n    public void testStatsForDifferentUsers() throws IOException {\n        hub.emulate().newSessions(1);\n        new RemoteWebDriver(hubUrl(gridRouter.baseUrlWithAuth), firefox());\n        assertThat(getActual(USER_1), is(newCountMap(\"firefox\", \"32.0\")));\n        assertThat(getActual(USER_2), is(empty()));\n    }\n\n    @Test\n    public void testEvictionOfOldSession() throws Exception {\n        hub.emulate().newSessions(1);\n        new RemoteWebDriver(hubUrl(gridRouter.baseUrlWithAuth), firefox());\n        Thread.sleep(1000);\n        assertThat(getActual(USER_1), is(newCountMap(\"firefox\", \"32.0\")));\n        Thread.sleep(6000);\n        assertThat(getActual(USER_1), is(empty()));\n    }\n\n    @Test\n    public void testActiveSessionIsNotEvicted() throws Exception {\n        hub.emulate().newSessions(1).navigation();\n        WebDriver driver = new RemoteWebDriver(hubUrl(gridRouter.baseUrlWithAuth), firefox());\n        for (int i = 0; i < 3; i++) {\n            Thread.sleep(2000);\n            driver.getCurrentUrl();\n            driver.get(\"http://yandex.ru\");\n        }\n        assertThat(getActual(USER_1), is(newCountMap(\"firefox\", \"32.0\")));\n    }\n\n    private BrowsersCountMap getActual(String user) throws IOException {\n        return executeSimpleGet(gridRouter.baseUrl(user) + \"/stats\", BrowsersCountMap.class);\n    }\n\n    private BrowsersCountMap newCountMap(String browser, String version) {\n        BrowsersCountMap expected = new BrowsersCountMap();\n        expected.increment(browser, version);\n        return expected;\n    }\n\n    private BrowsersCountMap empty() {\n        return new BrowsersCountMap();\n    }\n}\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/caps/AppiumCapabilityProcessorTest.java",
    "content": "package ru.qatools.gridrouter.caps;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.openqa.selenium.Platform;\nimport org.openqa.selenium.remote.DesiredCapabilities;\nimport ru.qatools.gridrouter.json.JsonCapabilities;\nimport ru.qatools.gridrouter.utils.JsonUtils;\n\nimport java.io.IOException;\n\nimport static org.hamcrest.Matchers.contains;\nimport static org.hamcrest.Matchers.equalTo;\nimport static org.hamcrest.Matchers.is;\nimport static org.junit.Assert.assertThat;\n\npublic class AppiumCapabilityProcessorTest {\n\n    private CapabilityProcessor processor;\n\n    @Before\n    public void setUp() throws Exception {\n        processor = new AppiumCapabilityProcessor();\n    }\n\n    @Test\n    public void accept() throws Exception {\n        assertThat(processor.accept(createCapabilities(\"\", \"iOS\")), is(true));\n        assertThat(processor.accept(createCapabilities(\"blabla\", \"iOS\")), is(false));\n        assertThat(processor.accept(createCapabilities(\"\", \"bla\")), is(false));\n        assertThat(processor.accept(createCapabilities(\"bla\", \"iOS\")), is(false));\n    }\n\n    private JsonCapabilities createCapabilities(String browserName, String platformName) throws IOException {\n        DesiredCapabilities desiredCapabilities = new DesiredCapabilities(browserName, \"test\", Platform.ANY);\n        desiredCapabilities.setCapability(\"platformName\", platformName);\n        return JsonUtils.buildJsonCapabilities(desiredCapabilities);\n    }\n    \n    @Test\n    public void process() throws Exception {\n        JsonCapabilities jsonCapabilities = new JsonCapabilities();\n        processor.process(jsonCapabilities);\n        assertThat(jsonCapabilities.any().keySet(), contains(\"keepKeyChains\"));\n        assertThat(jsonCapabilities.any().get(\"keepKeyChains\"), equalTo(true));\n    }\n\n}"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/caps/CapabilityProcessorFactoryTest.java",
    "content": "package ru.qatools.gridrouter.caps;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.springframework.beans.factory.annotation.Autowired;\nimport org.springframework.test.context.ContextConfiguration;\nimport org.springframework.test.context.junit4.SpringJUnit4ClassRunner;\nimport ru.qatools.gridrouter.json.JsonCapabilities;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.openqa.selenium.remote.DesiredCapabilities.firefox;\nimport static org.openqa.selenium.remote.DesiredCapabilities.internetExplorer;\nimport static ru.qatools.gridrouter.utils.JsonUtils.buildJsonCapabilities;\nimport static org.hamcrest.Matchers.*;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\n@RunWith(SpringJUnit4ClassRunner.class)\n@ContextConfiguration(\"classpath:META-INF/spring/application-context.xml\")\npublic class CapabilityProcessorFactoryTest {\n\n    @Autowired\n    private CapabilityProcessorFactory factory;\n\n    @Test\n    public void testGetIEProcessor() throws Exception {\n        JsonCapabilities ieCaps = buildJsonCapabilities(internetExplorer());\n        assertThat(factory.getProcessor(ieCaps), is(instanceOf(IECapabilityProcessor.class)));\n    }\n\n    @Test\n    public void testGetDummyProcessor() throws Exception {\n        JsonCapabilities firefoxCaps = buildJsonCapabilities(firefox());\n        assertThat(factory.getProcessor(firefoxCaps), is(instanceOf(DummyCapabilityProcessor.class)));\n    }\n}\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/caps/IECapabilityProcessorTest.java",
    "content": "package ru.qatools.gridrouter.caps;\n\nimport org.json.JSONObject;\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.openqa.selenium.remote.DesiredCapabilities;\nimport ru.qatools.gridrouter.json.JsonCapabilities;\nimport ru.qatools.gridrouter.json.JsonMessage;\nimport ru.qatools.gridrouter.json.Proxy;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.*;\nimport static org.openqa.selenium.Proxy.ProxyType.DIRECT;\nimport static org.openqa.selenium.remote.BrowserType.IE;\nimport static org.openqa.selenium.remote.CapabilityType.PROXY;\nimport static org.openqa.selenium.remote.DesiredCapabilities.firefox;\nimport static org.openqa.selenium.remote.DesiredCapabilities.internetExplorer;\nimport static ru.qatools.gridrouter.utils.JsonUtils.buildJsonCapabilities;\nimport static ru.qatools.gridrouter.utils.JsonUtils.buildJsonMessage;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic class IECapabilityProcessorTest {\n\n    private IECapabilityProcessor processor;\n\n    @Before\n    public void setUp() throws Exception {\n        processor = new IECapabilityProcessor();\n    }\n\n    @Test\n    public void testAccept() throws Exception {\n        assertThat(processor.accept(buildJsonCapabilities(internetExplorer())), is(true));\n        assertThat(processor.accept(buildJsonCapabilities(firefox())), is(false));\n    }\n\n    @Test\n    public void testAddProxy() throws Exception {\n        String version = \"11\";\n        JsonCapabilities capabilities = buildJsonCapabilities(internetExplorer(), version);\n\n        processor.process(capabilities);\n\n        assertThat(capabilities.getBrowserName(), is(equalTo(IE)));\n        assertThat(capabilities.getVersion(), is(equalTo(version)));\n        assertThat(capabilities.any().get(PROXY), is(notNullValue()));\n        assertThat(((Proxy) capabilities.any().get(PROXY)).getProxyType(), is(equalTo(DIRECT.name())));\n    }\n\n    @Test\n    public void testJsonMarshalling() throws Exception {\n        JsonMessage message = buildJsonMessage(internetExplorer());\n        processor.process(message.getDesiredCapabilities());\n        String proxyType = (String) new JSONObject(message.toJson())\n                .getJSONObject(\"desiredCapabilities\")\n                .getJSONObject(\"proxy\")\n                .get(\"proxyType\");\n        assertThat(proxyType, is(equalTo(DIRECT.name())));\n    }\n\n    @Test\n    public void testExistingProxyIsNotOverridden() throws Exception {\n        DesiredCapabilities caps = internetExplorer();\n        org.openqa.selenium.Proxy proxy = new org.openqa.selenium.Proxy();\n        proxy.setHttpProxy(PROXY);\n        caps.setCapability(PROXY, proxy);\n        JsonCapabilities capabilities = buildJsonCapabilities(caps);\n\n        processor.process(capabilities);\n\n        assertThat(capabilities.any().get(PROXY), not(instanceOf(Proxy.class)));\n    }\n}\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/json/JsonMessageTest.java",
    "content": "package ru.qatools.gridrouter.json;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport org.json.JSONObject;\nimport org.junit.Test;\n\nimport java.io.IOException;\n\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.is;\nimport static org.hamcrest.Matchers.nullValue;\nimport static ru.qatools.gridrouter.json.WithErrorMessage.DEFAULT_ERROR_MESSAGE;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic class JsonMessageTest {\n\n    @Test\n    public void testProperJson() throws IOException {\n        JSONObject jsonObject = new JSONObject();\n\n        jsonObject.put(\"status\", 69);\n        jsonObject.put(\"sessionId\", \"session id\");\n        jsonObject.put(\"some other key\", \"some other value\");\n\n        JSONObject capabilitiesObject = new JSONObject();\n        capabilitiesObject.put(\"browserName\", \"firefox\");\n        capabilitiesObject.put(\"version\", \"32.0\");\n        capabilitiesObject.put(\"some capability key\", \"some capability value\");\n        jsonObject.put(\"desiredCapabilities\", capabilitiesObject);\n\n        JSONObject valueObject = new JSONObject();\n        valueObject.put(\"message\", \"some error message\");\n        valueObject.put(\"some value key\", \"some value value\");\n        jsonObject.put(\"value\", valueObject);\n\n        JsonMessage jsonMessage = JsonMessageFactory.from(jsonObject.toString());\n\n        assertThat(jsonMessage.getStatus(), is(69));\n        assertThat(jsonMessage.getSessionId(), is(\"session id\"));\n        assertThat(jsonMessage.any().get(\"some other key\"), is(\"some other value\"));\n\n        JsonCapabilities jsonCapabilities = jsonMessage.getDesiredCapabilities();\n        assertThat(jsonCapabilities.getBrowserName(), is(\"firefox\"));\n        assertThat(jsonCapabilities.getVersion(), is(\"32.0\"));\n        assertThat(jsonCapabilities.any().get(\"some capability key\"), is(\"some capability value\"));\n\n        assertThat(jsonMessage.getErrorMessage(), is(\"some error message\"));\n    }\n\n    @Test\n    public void testJsonWithKeysMissing() throws IOException {\n        JSONObject jsonObject = new JSONObject();\n        jsonObject.put(\"status\", 69);\n\n        JsonMessage jsonMessage = JsonMessageFactory.from(jsonObject.toString());\n\n        assertThat(jsonMessage.getStatus(), is(69));\n        assertThat(jsonMessage.getSessionId(), is(nullValue()));\n        assertThat(jsonMessage.getDesiredCapabilities(), is(nullValue()));\n    }\n\n    @Test\n    public void testErrorMessageForNullValue() throws IOException {\n        JSONObject jsonObject = new JSONObject();\n        JsonMessage jsonMessage = JsonMessageFactory.from(jsonObject.toString());\n        assertThat(jsonMessage.getErrorMessage(), is(DEFAULT_ERROR_MESSAGE));\n    }\n\n    @Test\n    public void testNullErrorMessageForPresentValue() throws IOException {\n        JSONObject jsonObject = new JSONObject();\n        jsonObject.put(\"value\", new JSONObject());\n        JsonMessage jsonMessage = JsonMessageFactory.from(jsonObject.toString());\n        assertThat(jsonMessage.getErrorMessage(), is(DEFAULT_ERROR_MESSAGE));\n    }\n\n    @Test\n    public void testValueOfSimpleType() throws IOException {\n        String jsonRaw =\n                \"{\"\n                    + \"\\\"using\\\":\\\"xpath\\\",\"\n                    + \"\\\"value\\\":\\\"//lol[foo='bar']\\\"\"\n                + \"}\";\n        JsonMessage jsonMessage = JsonMessageFactory.from(jsonRaw);\n\n        assertThat(jsonMessage.getSessionId(), is(nullValue()));\n        assertThat(jsonMessage.any().get(\"value\"), is(\"//lol[foo='bar']\"));\n    }\n\n    @Test\n    public void testJsonView() throws JsonProcessingException {\n        JsonMessage jsonMessage = new JsonMessage();\n\n        jsonMessage.setSessionId(\"session id\");\n        jsonMessage.setStatus(69);\n\n        JsonCapabilities jsonCapabilities = new JsonCapabilities();\n        jsonCapabilities.setBrowserName(\"browser name\");\n        jsonCapabilities.setVersion(\"browser version\");\n        jsonMessage.setDesiredCapabilities(jsonCapabilities);\n\n        jsonMessage.set(\"some key\", \"some value\");\n\n        JSONObject jsonObject = new JSONObject(jsonMessage.toJson());\n        assertThat(jsonObject.getString(\"sessionId\"), is(\"session id\"));\n        assertThat(jsonObject.getInt(\"status\"), is(69));\n\n        JSONObject capabilitiesObject = jsonObject.getJSONObject(\"desiredCapabilities\");\n        assertThat(capabilitiesObject.get(\"browserName\"), is(\"browser name\"));\n        assertThat(capabilitiesObject.get(\"version\"), is(\"browser version\"));\n\n        assertThat(jsonObject.isNull(\"value\"), is(true));\n        assertThat(jsonObject.isNull(\"message\"), is(true));\n        assertThat(jsonObject.isNull(\"errorMessage\"), is(true));\n    }\n\n    @Test\n    public void testSettingErrorMessage() throws JsonProcessingException {\n        JsonMessage jsonMessage = JsonMessageFactory.error(69, \"some error message\");\n        JSONObject jsonObject = new JSONObject(jsonMessage.toJson());\n        assertThat(jsonObject.getInt(\"status\"), is(69));\n        assertThat(jsonObject.getJSONObject(\"value\").getString(\"message\"), is(\"some error message\"));\n\n    }\n}\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/sessions/MemoryStatsCounterTest.java",
    "content": "package ru.qatools.gridrouter.sessions;\n\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport org.junit.Before;\nimport org.junit.Test;\n\nimport java.time.Duration;\nimport java.util.HashSet;\nimport java.util.Set;\n\nimport static java.time.Duration.ZERO;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.*;\nimport static ru.qatools.gridrouter.json.JsonFormatter.toJson;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic class MemoryStatsCounterTest {\n\n    private MemoryStatsCounter storage;\n\n    @Before\n    public void setUp() throws Exception {\n        storage = new MemoryStatsCounter();\n    }\n\n    @Test\n    public void testEmptyStorage() throws Exception {\n        assertThat(countJsonFor(\"user\"), is(\"{}\"));\n        assertThat(expiredSessions(ZERO), is(empty()));\n        assertThat(expiredSessions(Duration.ofDays(1)), is(empty()));\n    }\n\n    @Test\n    public void testAddSession() throws Exception {\n        storage.startSession(\"session1\", \"user\", \"firefox\", \"33\");\n        storage.startSession(\"session2\", \"user\", \"firefox\", \"33\");\n        storage.startSession(\"session3\", \"user\", \"firefox\", \"33\");\n        assertThat(countJsonFor(\"user\"), is(\"{\\\"firefox\\\":{\\\"33\\\":3}}\"));\n        assertThat(storage.getSessionsCountForUser(\"user\"), is(3));\n        assertThat(storage.getSessionsCountForUserAndBrowser(\"user\", \"firefox\", \"33\"), is(3));\n        storage.startSession(\"session1\", \"user\", \"firefox\", \"33\");\n        assertThat(countJsonFor(\"user\"), is(\"{\\\"firefox\\\":{\\\"33\\\":3}}\"));\n        assertThat(storage.getSessionsCountForUser(\"user\"), is(3));\n        assertThat(storage.getSessionsCountForUserAndBrowser(\"user\", \"firefox\", \"33\"), is(3));\n    }\n\n    @Test\n    public void testDifferentBrowsers() throws Exception {\n        storage.startSession(\"session1\", \"user\", \"chrome\", \"33\");\n        storage.startSession(\"session2\", \"user\", \"firefox\", \"33\");\n        storage.startSession(\"session3\", \"user\", \"firefox\", \"33\");\n        assertThat(countJsonFor(\"user\"), is(\"{\\\"chrome\\\":{\\\"33\\\":1},\\\"firefox\\\":{\\\"33\\\":2}}\"));\n        assertThat(storage.getSessionsCountForUser(\"user\"), is(3));\n        assertThat(storage.getSessionsCountForUserAndBrowser(\"user\", \"firefox\", \"33\"), is(2));\n        assertThat(storage.getSessionsCountForUserAndBrowser(\"user\", \"chrome\", \"33\"), is(1));\n    }\n\n    @Test\n    public void testDifferentVersions() throws Exception {\n        storage.startSession(\"session1\", \"user\", \"firefox\", \"33\");\n        storage.startSession(\"session2\", \"user\", \"firefox\", \"34\");\n        storage.startSession(\"session3\", \"user\", \"firefox\", \"34\");\n        storage.startSession(\"session4\", \"user\", \"firefox\", \"firefox\");\n        storage.startSession(\"session5\", \"user\", \"firefox\", \"firefox\");\n        storage.startSession(\"session6\", \"user\", \"firefox\", \"firefox\");\n        assertThat(countJsonFor(\"user\"), is(\"{\\\"firefox\\\":{\\\"33\\\":1,\\\"34\\\":2,\\\"firefox\\\":3}}\"));\n        assertThat(storage.getSessionsCountForUser(\"user\"), is(6));\n        assertThat(storage.getSessionsCountForUserAndBrowser(\"user\", \"firefox\", \"33\"), is(1));\n        assertThat(storage.getSessionsCountForUserAndBrowser(\"user\", \"firefox\", \"34\"), is(2));\n        assertThat(storage.getSessionsCountForUserAndBrowser(\"user\", \"firefox\", \"firefox\"), is(3));\n    }\n\n    @Test\n    public void testRemoveExistingSession() throws Exception {\n        storage.startSession(\"session1\", \"user\", \"firefox\", \"33\");\n        storage.startSession(\"session2\", \"user\", \"firefox\", \"33\");\n        assertThat(countJsonFor(\"user\"), is(\"{\\\"firefox\\\":{\\\"33\\\":2}}\"));\n        assertThat(storage.getSessionsCountForUserAndBrowser(\"user\", \"firefox\", \"33\"), is(2));\n        storage.deleteSession(\"session1\");\n        assertThat(countJsonFor(\"user\"), is(\"{\\\"firefox\\\":{\\\"33\\\":1}}\"));\n        assertThat(storage.getSessionsCountForUserAndBrowser(\"user\", \"firefox\", \"33\"), is(1));\n        storage.deleteSession(\"session1\");\n        assertThat(countJsonFor(\"user\"), is(\"{\\\"firefox\\\":{\\\"33\\\":1}}\"));\n        assertThat(storage.getSessionsCountForUserAndBrowser(\"user\", \"firefox\", \"33\"), is(1));\n        storage.deleteSession(\"session2\");\n        assertThat(countJsonFor(\"user\"), is(\"{}\"));\n        assertThat(storage.getSessionsCountForUserAndBrowser(\"user\", \"firefox\", \"33\"), is(0));\n    }\n\n    @Test\n    public void testRemoveNotExistingSession() throws Exception {\n        storage.deleteSession(\"session1\");\n        storage.startSession(\"session1\", \"user\", \"firefox\", \"33\");\n        storage.startSession(\"session2\", \"user\", \"firefox\", \"33\");\n        assertThat(countJsonFor(\"user\"), is(\"{\\\"firefox\\\":{\\\"33\\\":2}}\"));\n        storage.deleteSession(\"session4\");\n        assertThat(countJsonFor(\"user\"), is(\"{\\\"firefox\\\":{\\\"33\\\":2}}\"));\n    }\n\n    @Test\n    public void testMultipleUsers() throws Exception {\n        storage.startSession(\"session1\", \"user1\", \"firefox\", \"33\");\n        storage.startSession(\"session2\", \"user2\", \"firefox\", \"33\");\n        storage.startSession(\"session3\", \"user2\", \"firefox\", \"33\");\n        assertThat(countJsonFor(\"user1\"), is(\"{\\\"firefox\\\":{\\\"33\\\":1}}\"));\n        assertThat(countJsonFor(\"user2\"), is(\"{\\\"firefox\\\":{\\\"33\\\":2}}\"));\n        storage.deleteSession(\"session1\");\n        storage.deleteSession(\"session2\");\n        assertThat(countJsonFor(\"user1\"), is(\"{}\"));\n        assertThat(countJsonFor(\"user2\"), is(\"{\\\"firefox\\\":{\\\"33\\\":1}}\"));\n    }\n\n    @Test\n    public void testNewSessionsAreNotExpired() throws Exception {\n        storage.startSession(\"session1\", \"user\", \"firefox\", \"33\");\n        storage.startSession(\"session2\", \"user\", \"firefox\", \"33\");\n        assertThat(expiredSessions(1000), is(empty()));\n        assertThat(countJsonFor(\"user\"), is(\"{\\\"firefox\\\":{\\\"33\\\":2}}\"));\n    }\n\n    @Test\n    public void testOldSessionsAreExpired() throws Exception {\n        storage.startSession(\"session1\", \"user\", \"firefox\", \"33\");\n        storage.startSession(\"session2\", \"user\", \"firefox\", \"33\");\n        Thread.sleep(500);\n        storage.startSession(\"session3\", \"user\", \"firefox\", \"33\");\n        assertThat(expiredSessions(250), containsInAnyOrder(\"session1\", \"session2\"));\n        assertThat(countJsonFor(\"user\"), is(\"{\\\"firefox\\\":{\\\"33\\\":1}}\"));\n        Thread.sleep(500);\n        assertThat(expiredSessions(250), contains(\"session3\"));\n        assertThat(countJsonFor(\"user\"), is(\"{}\"));\n    }\n\n    @Test\n    public void testUpdateExistingSession() throws Exception {\n        storage.startSession(\"session1\", \"user\", \"firefox\", \"33\");\n        Thread.sleep(500);\n        storage.updateSession(\"session1\");\n        assertThat(expiredSessions(250), is(empty()));\n    }\n\n    @Test\n    public void testMultipleUsersExpiration() throws Exception {\n        storage.startSession(\"session1\", \"user1\", \"firefox\", \"33\");\n        Thread.sleep(500);\n        storage.startSession(\"session2\", \"user2\", \"firefox\", \"33\");\n        assertThat(expiredSessions(250), contains(\"session1\"));\n        assertThat(countJsonFor(\"user1\"), is(\"{}\"));\n        assertThat(countJsonFor(\"user2\"), is(\"{\\\"firefox\\\":{\\\"33\\\":1}}\"));\n    }\n\n    private String countJsonFor(String user) throws JsonProcessingException {\n        return toJson(storage.getStats(user));\n    }\n\n    public Set<String> expiredSessions(int millis) {\n        return expiredSessions(Duration.ofMillis(millis));\n    }\n\n    public Set<String> expiredSessions(Duration duration) {\n        final Set<String> removedSessionIds = new HashSet<>(storage.getActiveSessions());\n        storage.expireSessionsOlderThan(duration);\n        removedSessionIds.removeAll(storage.getActiveSessions());\n        return removedSessionIds;\n    }\n}\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/sessions/WaitAvailableBrowsersCheckerTest.java",
    "content": "package ru.qatools.gridrouter.sessions;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport ru.qatools.gridrouter.config.Version;\n\nimport java.time.Duration;\nimport java.time.temporal.Temporal;\n\nimport static java.time.ZonedDateTime.now;\nimport static org.hamcrest.MatcherAssert.assertThat;\nimport static org.hamcrest.Matchers.greaterThanOrEqualTo;\nimport static org.hamcrest.Matchers.lessThan;\nimport static org.mockito.Matchers.eq;\nimport static org.mockito.Mockito.*;\n\n/**\n * @author Ilya Sadykov\n */\npublic class WaitAvailableBrowsersCheckerTest {\n    WaitAvailableBrowsersChecker checker;\n    Version version;\n    StatsCounter counter;\n\n    @Before\n    public void setUp() throws Exception {\n        counter = mock(StatsCounter.class);\n        checker = new WaitAvailableBrowsersChecker(3, 1, counter);\n        version = new Version();\n        version.setPermittedCount(10);\n        version.setNumber(\"33\");\n        when(counter.getSessionsCountForUserAndBrowser(eq(\"user\"), eq(\"firefox\"), eq(\"33\"))).thenReturn(10);\n    }\n\n    @Test\n    public void testWaitAvailableBrowsersChecker() throws Exception {\n        Temporal started = now();\n        try {\n            checker.ensureFreeBrowsersAvailable(\"user\", \"host\", \"firefox\", version);\n        } catch (WaitAvailableBrowserTimeoutException e) {\n            // do nothing\n        }\n        verify(counter, times(3)).getSessionsCountForUserAndBrowser(eq(\"user\"), eq(\"firefox\"), eq(\"33\"));\n        assertThat(Duration.between(started, now()).toMillis(), greaterThanOrEqualTo(3000L));\n    }\n\n    @Test(expected = WaitAvailableBrowserTimeoutException.class)\n    public void testWaitAvailableBrowsersTimeout() throws Exception {\n        checker.ensureFreeBrowsersAvailable(\"user\", \"host\", \"firefox\", version);\n    }\n\n    @Test\n    public void testNoWaitAvailableBrowser() throws Exception {\n        when(counter.getSessionsCountForUserAndBrowser(eq(\"user\"), eq(\"firefox\"), eq(\"33\"))).thenReturn(5);\n\n        Temporal started = now();\n        checker.ensureFreeBrowsersAvailable(\"user\", \"host\", \"firefox\", version);\n        verify(counter, times(1)).getSessionsCountForUserAndBrowser(eq(\"user\"), eq(\"firefox\"), eq(\"33\"));\n        assertThat(Duration.between(started, now()).toMillis(), lessThan(1000L));\n        verifyNoMoreInteractions(counter);\n    }\n}"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/utils/FindElementCallback.java",
    "content": "package ru.qatools.gridrouter.utils;\n\nimport org.json.JSONObject;\nimport org.mockserver.mock.action.ExpectationCallback;\nimport org.mockserver.model.HttpRequest;\nimport org.mockserver.model.HttpResponse;\n\nimport static org.mockserver.model.HttpResponse.response;\n\n/**\n * Sets the element id (according to protocol specification)\n * to the hashcode of the selector. This way we can check that\n * the selector was passed through proxy correctly.\n *\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic class FindElementCallback implements ExpectationCallback {\n\n    @Override\n    public HttpResponse handle(HttpRequest httpRequest) {\n        JSONObject jsonObject = new JSONObject(httpRequest.getBodyAsString());\n        String selector = jsonObject.get(\"value\").toString();\n\n        JSONObject responce = new JSONObject();\n        responce.put(\"status\", 0);\n        JSONObject value = new JSONObject();\n        value.put(\"ELEMENT\", selector.hashCode());\n        responce.put(\"value\", value);\n        return response(responce.toString()).withStatusCode(500);\n    }\n}\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/utils/GridRouterRule.java",
    "content": "package ru.qatools.gridrouter.utils;\n\nimport org.eclipse.jetty.security.HashLoginService;\nimport org.eclipse.jetty.util.security.Password;\n\nimport java.net.MalformedURLException;\nimport java.net.URL;\n\nimport static java.util.UUID.randomUUID;\nimport static ru.qatools.gridrouter.utils.SocketUtil.findFreePort;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic class GridRouterRule extends JettyRule {\n\n    public static final String USER_1 = \"user1\";\n    public static final String USER_2 = \"user2\";\n    public static final String USER_3 = \"user3\";\n    public static final String USER_4 = \"user4\";\n    public static final String PASSWORD = \"password\";\n    public static final String ROLE = \"user\";\n\n    public final String baseUrl;\n    public final String baseUrlWithAuth;\n    public final String baseUrlWrongPassword;\n\n\n    public GridRouterRule() {\n        super(\n                \"/\",\n                \"src/main/webapp\",\n                \"target/classes\",\n                findFreePort(),\n                new HashLoginService() {{\n                    setName(\"Selenium Grid Router\");\n                    putUser(USER_1, new Password(PASSWORD), new String[]{ROLE});\n                    putUser(USER_2, new Password(PASSWORD), new String[]{ROLE});\n                    putUser(USER_3, new Password(PASSWORD), new String[]{ROLE});\n                    putUser(USER_4, new Password(PASSWORD), new String[]{ROLE});\n                }}\n        );\n        baseUrl = \"http://localhost:\" + getPort();\n        baseUrlWithAuth = baseUrl(USER_1);\n        baseUrlWrongPassword = baseUrl(USER_1, randomUUID().toString());\n    }\n\n    public static URL hubUrl(String baseUrl) {\n        try {\n            return new URL(baseUrl + \"/wd/hub\");\n        } catch (MalformedURLException e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public String baseUrl(String user) {\n        return baseUrl(user, PASSWORD);\n    }\n\n    public String baseUrl(String user, String password) {\n        return String.format(\"http://%s:%s@localhost:%d\",\n                user, password, getPort());\n    }\n}\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/utils/HttpUtils.java",
    "content": "package ru.qatools.gridrouter.utils;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport org.apache.http.client.methods.CloseableHttpResponse;\nimport org.apache.http.client.methods.HttpGet;\nimport org.apache.http.impl.client.HttpClientBuilder;\n\nimport java.io.IOException;\nimport java.io.InputStream;\n\n/**\n * @author Dmitry Baev charlie@yandex-team.ru\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic final class HttpUtils {\n\n    private HttpUtils() {\n    }\n\n    public static <T> T executeSimpleGet(String url, Class<T> clazz) throws IOException {\n        CloseableHttpResponse execute = HttpClientBuilder\n                .create().build()\n                .execute(new HttpGet(url));\n        InputStream content = execute.getEntity().getContent();\n        return new ObjectMapper().readValue(content, clazz);\n    }\n}\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/utils/HubEmulator.java",
    "content": "package ru.qatools.gridrouter.utils;\n\nimport org.json.JSONObject;\nimport org.mockserver.integration.ClientAndServer;\nimport org.mockserver.matchers.Times;\nimport org.mockserver.model.HttpRequest;\nimport org.mockserver.model.HttpResponse;\n\nimport java.util.concurrent.TimeUnit;\n\nimport static java.util.UUID.randomUUID;\nimport static org.mockserver.integration.ClientAndServer.startClientAndServer;\nimport static org.mockserver.matchers.Times.once;\nimport static org.mockserver.model.HttpCallback.callback;\nimport static org.mockserver.model.HttpRequest.request;\nimport static org.mockserver.model.HttpResponse.response;\nimport static org.mockserver.verify.VerificationTimes.exactly;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic class HubEmulator {\n\n    private static final String WD_HUB_SESSION = \"/wd/hub/session\";\n\n    private static final String SESSION_ID_REGEX = \"[-a-zA-Z0-9]{36}\";\n\n    private ClientAndServer hub;\n\n    public HubEmulator(int hubPort) {\n        hub = startClientAndServer(hubPort);\n    }\n\n    public HubEmulations emulate() {\n        return new HubEmulations();\n    }\n\n    public HubVerifications verify() {\n        return new HubVerifications();\n    }\n\n    public void stop() {\n        hub.stop();\n    }\n\n    public class HubEmulations {\n\n        public HubEmulations newSessions(int sessionsCount) {\n            for (int i = 0; i < sessionsCount; i++) {\n                hub.when(newSessionRequest(), once()).respond(newSessionSuccessful());\n            }\n            return this;\n        }\n\n        public HubEmulations newSessionFailures(int times) {\n            return newSessionFailures(Times.exactly(times));\n        }\n\n        public HubEmulations newSessionFailures(Times times) {\n            hub.when(newSessionRequest(), times).respond(newSessionFailed());\n            return this;\n        }\n\n        public HubEmulations newSessionFreeze(int seconds) {\n            hub.when(newSessionRequest(), once()).respond(\n                    response()\n                            .withDelay(TimeUnit.SECONDS, seconds)\n                            .withStatusCode(500)\n            );\n            return this;\n        }\n        \n        public HubEmulations navigation() {\n            hub.when(sessionRequest(\"url\"))\n               .callback(callback().withCallbackClass(\n                       RememberUrlCallback.class.getCanonicalName()));\n            return this;\n        }\n\n        public HubEmulations findElement() {\n            hub.when(sessionRequest(\"element\").withMethod(\"POST\"))\n               .callback(callback().withCallbackClass(\n                       FindElementCallback.class.getCanonicalName()));\n            return this;\n        }\n\n        public HubEmulations quit() {\n            hub.when(sessionQuitRequest()).respond(emptyResponse());\n            return this;\n        }\n    }\n\n    public class HubVerifications {\n\n        public HubVerifications newSessionRequestsCountIs(int sessionsCount) {\n            hub.verify(newSessionRequest(), exactly(sessionsCount));\n            return this;\n        }\n\n        public HubVerifications quitRequestsCountIs(int times) {\n            hub.verify(sessionQuitRequest(), exactly(times));\n            return this;\n        }\n\n        public HubVerifications totalRequestsCountIs(int times) {\n            hub.verify(request(\".*\"), exactly(times));\n            return this;\n        }\n    }\n\n    private static HttpRequest newSessionRequest() {\n        return request(WD_HUB_SESSION).withMethod(\"POST\");\n    }\n\n    private static HttpRequest sessionRequest(String handler) {\n        return request(WD_HUB_SESSION + \"/\" + SESSION_ID_REGEX + \"/\" + handler);\n    }\n\n    private static HttpRequest sessionQuitRequest() {\n        return request(WD_HUB_SESSION +\"/.*\").withMethod(\"DELETE\");\n    }\n\n    private HttpResponse emptyResponse() {\n        JSONObject json = new JSONObject();\n        json.put(\"value\", new JSONObject());\n        return response(json.toString());\n    }\n\n\n    private static HttpResponse newSessionSuccessful() {\n        JSONObject json = new JSONObject();\n        json.put(\"value\", new JSONObject());\n        json.put(\"sessionId\", randomUUID());\n        return response(json.toString());\n    }\n\n    private static HttpResponse newSessionFailed() {\n        JSONObject json = new JSONObject();\n        json.put(\"status\", 6);\n        JSONObject value = new JSONObject();\n        value.put(\"message\", \"unable to start browser\");\n        json.put(\"value\", value);\n        return response(json.toString()).withStatusCode(500);\n    }\n}\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/utils/HubEmulatorRule.java",
    "content": "package ru.qatools.gridrouter.utils;\n\nimport org.junit.rules.TestWatcher;\nimport org.junit.runner.Description;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\nimport java.util.function.Consumer;\n\nimport static ru.qatools.gridrouter.utils.SocketUtil.findFreePort;\nimport static ru.qatools.gridrouter.utils.TestConfigRepository.changePort;\nimport static ru.qatools.gridrouter.utils.TestConfigRepository.resetConfig;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic class HubEmulatorRule extends TestWatcher {\n    static final Logger LOGGER = LoggerFactory.getLogger(HubEmulatorRule.class);\n    private int fromPort;\n    private int port;\n    private HubEmulator hub;\n\n    public HubEmulatorRule(int fromPort) {\n        this(fromPort, hub -> {\n        });\n    }\n\n    public HubEmulatorRule(int fromPort, Consumer<HubEmulator> initializer) {\n        this.fromPort = fromPort;\n        port = findFreePort();\n        LOGGER.info(\"Selected new free port {}, starting emulator...\", port);\n        hub = new HubEmulator(port);\n        if (initializer != null) {\n            LOGGER.info(\"Running initializer...\");\n            try {\n                initializer.accept(hub);\n            } catch (Exception e) {\n                throw new RuntimeException(e);\n            }\n        }\n        LOGGER.info(\"Waiting for config initialization...\");\n        changePort(fromPort, port);\n    }\n\n    @Override\n    protected void finished(Description description) {\n        resetConfig();\n        hub.stop();\n    }\n\n    public HubEmulator.HubEmulations emulate() {\n        return hub.emulate();\n    }\n\n    public HubEmulator.HubVerifications verify() {\n        return hub.verify();\n    }\n\n    public int getPort() {\n        return port;\n    }\n}\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/utils/JettyRule.java",
    "content": "package ru.qatools.gridrouter.utils;\n\nimport org.eclipse.jetty.annotations.AnnotationConfiguration;\nimport org.eclipse.jetty.server.Server;\nimport org.eclipse.jetty.webapp.Configuration;\nimport org.eclipse.jetty.webapp.WebAppContext;\nimport org.eclipse.jetty.webapp.WebInfConfiguration;\nimport org.eclipse.jetty.webapp.WebXmlConfiguration;\nimport org.junit.rules.TestRule;\nimport org.junit.runner.Description;\nimport org.junit.runners.model.Statement;\n\npublic class JettyRule implements TestRule {\n\n    private final String contextPath;\n    private final String classPath;\n    private final String warPath;\n    private final int port;\n    private Server server;\n\n    private Object[] beans;\n\n    public JettyRule(String contextPath, String warPath, String classPath, int port, Object... beans) {\n        this.contextPath = contextPath;\n        this.classPath = classPath;\n        this.warPath = warPath;\n        this.port = port;\n        this.beans = beans;\n    }\n\n    @Override\n    public Statement apply(final Statement base, Description description) {\n        return new Statement() {\n            @Override\n            public void evaluate() throws Throwable {\n                before();\n                try {\n                    base.evaluate();\n                } finally {\n                    after();\n                }\n            }\n        };\n    }\n\n    protected void before() throws Exception {\n        WebAppContext context = new WebAppContext();\n        context.setResourceBase(warPath);\n        context.setExtraClasspath(classPath);\n        context.setContextPath(contextPath);\n        context.setParentLoaderPriority(true);\n\n        context.setConfigurations(new Configuration[]{\n                new AnnotationConfiguration(),\n                new WebXmlConfiguration(),\n                new WebInfConfiguration()\n        });\n\n        server = new Server(port);\n        server.setHandler(context);\n        for (Object bean : beans) {\n            server.addBean(bean);\n        }\n        server.start();\n    }\n\n    protected void after() throws Exception {\n        server.stop();\n    }\n\n    public int getPort() {\n        return port;\n    }\n}\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/utils/JsonUtils.java",
    "content": "package ru.qatools.gridrouter.utils;\n\nimport org.json.JSONObject;\nimport org.openqa.selenium.remote.DesiredCapabilities;\nimport ru.qatools.gridrouter.json.JsonCapabilities;\nimport ru.qatools.gridrouter.json.JsonMessage;\nimport ru.qatools.gridrouter.json.JsonMessageFactory;\n\nimport java.io.IOException;\nimport java.util.Map;\n\nimport static org.openqa.selenium.remote.CapabilityType.BROWSER_NAME;\nimport static org.openqa.selenium.remote.CapabilityType.PLATFORM;\nimport static org.openqa.selenium.remote.CapabilityType.PROXY;\nimport static org.openqa.selenium.remote.CapabilityType.VERSION;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic final class JsonUtils {\n\n    private JsonUtils() {\n    }\n\n    public static JsonCapabilities buildJsonCapabilities(DesiredCapabilities capabilities)\n            throws IOException {\n        return buildJsonMessage(capabilities).getDesiredCapabilities();\n    }\n\n    public static JsonCapabilities buildJsonCapabilities(DesiredCapabilities capabilities, String version)\n            throws IOException {\n        capabilities.setVersion(version);\n        return buildJsonMessage(capabilities).getDesiredCapabilities();\n    }\n\n    public static JsonMessage buildJsonMessage(DesiredCapabilities capabilities) throws IOException {\n        JSONObject capabilitiesObject = new JSONObject();\n        Map<String, ?> capabilitiesMap = capabilities.asMap();\n        capabilitiesMap.keySet().forEach(k -> capabilitiesObject.put(k, capabilitiesMap.get(k)));\n        JSONObject jsonObject = new JSONObject();\n        jsonObject.put(\"desiredCapabilities\", capabilitiesObject);\n        return JsonMessageFactory.from(jsonObject.toString());\n    }\n}\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/utils/MatcherUtils.java",
    "content": "package ru.qatools.gridrouter.utils;\n\nimport org.hamcrest.Description;\nimport org.hamcrest.Matcher;\nimport org.hamcrest.TypeSafeMatcher;\nimport org.openqa.selenium.remote.DesiredCapabilities;\nimport org.openqa.selenium.remote.RemoteWebDriver;\n\nimport static ru.qatools.gridrouter.utils.GridRouterRule.hubUrl;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic final class MatcherUtils {\n\n    private MatcherUtils() {\n    }\n\n    /**\n     * Creates a matcher that tries to obtain a browser\n     * for a user that it is matched against.\n     *\n     * @return A matcher instance that creates a new webdriver\n     * on {@link Matcher#matches(Object) matches()} method invocation.\n     *\n     * @param browser capabilities for the browser to obtain\n     */\n    public static Matcher<String> canObtain(final GridRouterRule gridRouter, final DesiredCapabilities browser) {\n        return new TypeSafeMatcher<String>() {\n\n            private Exception exception;\n\n            @Override\n            protected boolean matchesSafely(String user) {\n                try {\n                    new RemoteWebDriver(hubUrl(gridRouter.baseUrl(user)), browser);\n                    return true;\n                } catch (Exception e) {\n                    exception = e;\n                }\n                return false;\n            }\n\n            @Override\n            public void describeTo(Description description) {\n                description.appendText(\"not able to obtain browser because of \")\n                           .appendValue(exception.toString());\n            }\n        };\n    }\n}\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/utils/QuotaUtils.java",
    "content": "package ru.qatools.gridrouter.utils;\n\nimport org.apache.commons.io.FileUtils;\nimport org.apache.commons.lang3.SerializationUtils;\nimport ru.qatools.gridrouter.config.Browsers;\n\nimport javax.xml.bind.JAXB;\nimport java.io.File;\nimport java.io.StringWriter;\n\nimport static java.lang.ClassLoader.getSystemResource;\nimport static ru.qatools.gridrouter.utils.GridRouterRule.USER_1;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n */\npublic final class QuotaUtils {\n\n    public static final String QUOTA_FILE_PATTERN\n            = getSystemResource(\"quota/\" + USER_1 + \".xml\").getPath().replace(USER_1, \"%s\");\n\n    private QuotaUtils() {\n    }\n\n    public static void replacePortInQuotaFile(String user, int port) {\n        replacePortInQuotaFile(user, 0, 0, port);\n    }\n\n    public static void replacePortInQuotaFile(String user, int regionNum, int hostNum, int port) {\n        copyQuotaFile(user, user, regionNum, hostNum, port);\n    }\n\n    public static void copyQuotaFile(String srcUser, String dstUser, int regionNum, int hostNum, int withHubPort) {\n        Browsers browsers = getQuotaFor(srcUser);\n        setPort(browsers, regionNum, hostNum, withHubPort);\n        writeQuotaFor(dstUser, browsers);\n    }\n\n    public static Browsers getQuotaFor(String user) {\n        File quotaFile = getQuotaFile(user);\n        Browsers browsersOriginal = JAXB.unmarshal(quotaFile, Browsers.class);\n        return SerializationUtils.clone(browsersOriginal);\n    }\n\n    public static synchronized void writeQuotaFor(String user, Browsers browsers) {\n        try {\n            //workaround to write the whole file at once\n            StringWriter xml = new StringWriter();\n            JAXB.marshal(browsers, xml);\n            final File fileToWrite = getQuotaFile(user);\n            final File tmpFile = File.createTempFile(user, \"xml\");\n            FileUtils.write(tmpFile, xml.toString());\n            FileUtils.copyFile(tmpFile, fileToWrite);\n            FileUtils.deleteQuietly(tmpFile);\n        } catch (Exception e) {\n            throw new RuntimeException(e);\n        }\n    }\n\n    public static File getQuotaFile(String user) {\n        return new File(String.format(QUOTA_FILE_PATTERN, user));\n    }\n\n    @SuppressWarnings(\"ResultOfMethodCallIgnored\")\n    public static void deleteQuotaFile(String user) {\n        getQuotaFile(user).delete();\n    }\n\n    public static void setPort(Browsers browsers, int regionNum, int hostNumber, int port) {\n        browsers.getBrowsers().get(0)\n                .getVersions().get(0)\n                .getRegions().get(regionNum)\n                .getHosts().get(hostNumber)\n                .setPort(port);\n    }\n}\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/utils/RememberUrlCallback.java",
    "content": "package ru.qatools.gridrouter.utils;\n\nimport org.json.JSONObject;\nimport org.mockserver.mock.action.ExpectationCallback;\nimport org.mockserver.model.HttpRequest;\nimport org.mockserver.model.HttpResponse;\n\nimport static org.mockserver.model.HttpResponse.response;\n\n/**\n * @author Innokenty Shuvalov innokenty@yandex-team.ru\n * @author Dmitry Baev charlie@yandex-team.ru\n */\npublic class RememberUrlCallback implements ExpectationCallback {\n\n    private static String currentUrl = \"{\\\"value\\\":\\\"\\\"}\";\n\n    @Override\n    public HttpResponse handle(HttpRequest httpRequest) {\n        if (httpRequest.getMethod().toString().contains(\"POST\")) {\n            JSONObject jsonObject = new JSONObject(httpRequest.getBodyAsString());\n            currentUrl = jsonObject.get(\"url\").toString();\n            return response();\n        } else if (httpRequest.getMethod().toString().contains(\"GET\")) {\n            return response(currentUrl);\n        }\n        return response(\"invalid request!\").withStatusCode(400);\n    }\n}\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/utils/SocketUtil.java",
    "content": "package ru.qatools.gridrouter.utils;\n\nimport java.io.IOException;\nimport java.net.ServerSocket;\n\npublic enum SocketUtil {\n    ;\n\n    /**\n     * Returns a free port number on localhost.\n     * Heavily inspired from org.eclipse.jdt.launching.SocketUtil (to avoid a dependency to JDT just because of this).\n     * Slightly improved with close() missing in JDT. And throws exception instead of returning -1.\n     *\n     * @return a free port number on localhost\n     * @throws IllegalStateException if unable to find a free port\n     */\n    public static int findFreePort() {\n        ServerSocket socket = null;\n        try {\n            socket = new ServerSocket(0);\n            socket.setReuseAddress(true);\n            int port = socket.getLocalPort();\n            try {\n                socket.close();\n            } catch (IOException ignored) {\n                // Ignore IOException on close()\n            }\n            return port;\n        } catch (IOException ignored) {\n            // Ignore IOException on open\n        } finally {\n            if (socket != null) {\n                try {\n                    socket.close();\n                } catch (IOException ignored) {\n                    // Ignore IOException on close()\n                }\n            }\n        }\n        throw new IllegalStateException(\"Could not find a free TCP/IP port to start embedded Jetty HTTP Server on\");\n    }\n}\n"
  },
  {
    "path": "proxy/src/test/java/ru/qatools/gridrouter/utils/TestConfigRepository.java",
    "content": "package ru.qatools.gridrouter.utils;\n\nimport org.apache.commons.io.FilenameUtils;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport ru.qatools.beanloader.BeanLoader;\nimport ru.qatools.gridrouter.ConfigRepository;\nimport ru.qatools.gridrouter.config.Browsers;\n\nimport java.io.IOException;\nimport java.io.StringReader;\nimport java.io.StringWriter;\nimport java.net.URISyntaxException;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport static java.util.Collections.unmodifiableMap;\nimport static javax.xml.bind.JAXB.marshal;\nimport static javax.xml.bind.JAXB.unmarshal;\n\n/**\n * @author Ilya Sadykov\n */\npublic class TestConfigRepository implements ConfigRepository {\n    protected static final String XML_GLOB = \"*.xml\";\n    private static final Logger LOGGER = LoggerFactory.getLogger(TestConfigRepository.class);\n    private static Map<String, Browsers> initialBrowsers = new HashMap<>();\n    private static Map<String, String> initialRoutes = new HashMap<>();\n    private static Map<String, Browsers> userBrowsers = new HashMap<>();\n    private static Map<String, String> routes = new HashMap<>();\n\n    static {\n        try {\n            final Path quotaDir = Paths.get(TestConfigRepository.class.getClassLoader().getResource(\"quota\").toURI());\n            LOGGER.debug(\"Loading quota configuration\");\n            initialBrowsers = new HashMap<>();\n            initialRoutes = new HashMap<>();\n            BeanLoader.loadAll(Browsers.class, quotaDir, XML_GLOB, (path, quota) -> {\n                String user = FilenameUtils.getBaseName(path.toString());\n                initialBrowsers.put(user, quota);\n                initialRoutes.putAll(quota.getRoutesMap());\n            });\n            initialBrowsers = unmodifiableMap(initialBrowsers);\n            initialRoutes = unmodifiableMap(initialRoutes);\n            resetConfig();\n        } catch (IOException | URISyntaxException e) {\n            LOGGER.error(\"Quota configuration loading failed\", e);\n        }\n    }\n\n    private static Browsers copy(Browsers quota) {\n        StringWriter writer = new StringWriter();\n        marshal(quota, writer);\n        return unmarshal(new StringReader(writer.toString()), Browsers.class);\n    }\n\n\n    public static synchronized void resetConfig() {\n        userBrowsers.clear();\n        initialBrowsers.entrySet().forEach(e -> {\n            userBrowsers.put(e.getKey(), copy(e.getValue()));\n        });\n        routes.clear();\n        routes.putAll(initialRoutes);\n    }\n\n    public static synchronized void changePort(int from, int to) {\n        userBrowsers.keySet().forEach(quotaName ->\n                userBrowsers.get(quotaName).getBrowsers().forEach(browser ->\n                        browser.getVersions().forEach(version ->\n                                version.getRegions().forEach(region ->\n                                        region.getHosts().forEach(host -> {\n                                            if (host.getPort() == from) {\n                                                LOGGER.info(\"Changing port of {} from {} to {} for user {}\",\n                                                        host, from, to, quotaName);\n                                                host.setPort(to);\n                                                routes.putAll(userBrowsers.get(quotaName).getRoutesMap());\n                                            }\n                                        })))));\n    }\n\n    @Override\n    public Map<String, Browsers> getQuotaMap() {\n        return userBrowsers;\n    }\n\n    @Override\n    public String getRoute(String routeId) {\n        return routes.get(routeId);\n    }\n\n}\n"
  },
  {
    "path": "proxy/src/test/resources/META-INF/spring/test-application-context.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<beans xmlns=\"http://www.springframework.org/schema/beans\"\n       xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n       xsi:schemaLocation=\"http://www.springframework.org/schema/beans\n      http://www.springframework.org/schema/beans/spring-beans-4.0.xsd\">\n\n    <import resource=\"classpath:META-INF/spring/application-context.xml\"/>\n\n    <bean id=\"hostSelectionStrategy\" class=\"ru.qatools.gridrouter.config.SequentialHostSelectionStrategy\"/>\n\n</beans>\n"
  },
  {
    "path": "proxy/src/test/resources/application.properties",
    "content": "grid.config.quota.directory=classpath:quota\ngrid.router.quota.repository=ru.qatools.gridrouter.utils.TestConfigRepository\ngrid.config.quota.hotReload=true\ngrid.router.evict.sessions.cron=* * * * * *\ngrid.router.evict.sessions.timeout.seconds=5\ngrid.router.route.timeout.seconds=5\n\ngrid.router.host.selection.strategy=ru.qatools.gridrouter.config.RandomHostSelectionStrategy\ngrid.router.stats.counter=ru.qatools.gridrouter.sessions.MemoryStatsCounter\ngrid.router.available.browsers.checker=ru.qatools.gridrouter.sessions.SkipAvailableBrowsersChecker\n"
  },
  {
    "path": "proxy/src/test/resources/log4j.properties",
    "content": "# suppress inspection \"UnusedProperty\" for whole file\nlog4j.rootLogger=INFO, out\n\n# CONSOLE appender not used by default\nlog4j.appender.out=org.apache.log4j.ConsoleAppender\nlog4j.appender.out.layout=org.apache.log4j.PatternLayout\nlog4j.appender.out.layout.ConversionPattern=%d %-5p %c - %m%n\nlog4j.throwableRenderer=org.apache.log4j.EnhancedThrowableRenderer\n\nlog4j.logger.org.mockserver.mockserver.MockServerHandler=WARN\nlog4j.logger.ru.qatools.gridrouter=DEBUG\n"
  },
  {
    "path": "proxy/src/test/resources/quota/user1.xml",
    "content": "<qa:browsers xmlns:qa=\"urn:config.gridrouter.qatools.ru\">\n    <browser name=\"firefox\" defaultVersion=\"32.0\">\n        <version number=\"32.0\">\n            <region name=\"first\">\n                <host name=\"localhost\" port=\"8081\" count=\"1\"/>\n            </region>\n        </version>\n    </browser>\n</qa:browsers>\n"
  },
  {
    "path": "proxy/src/test/resources/quota/user2.xml",
    "content": "<qa:browsers xmlns:qa=\"urn:config.gridrouter.qatools.ru\">\n    <browser name=\"firefox\" defaultVersion=\"32.0\">\n        <version number=\"32.0\">\n            <region name=\"first\">\n                <host name=\"localhost\" port=\"8081\" count=\"1\"/>\n                <host name=\"localhost\" port=\"8082\" count=\"3\"/>\n            </region>\n        </version>\n    </browser>\n</qa:browsers>\n"
  },
  {
    "path": "proxy/src/test/resources/quota/user3.xml",
    "content": "<qa:browsers xmlns:qa=\"urn:config.gridrouter.qatools.ru\">\n    <browser name=\"firefox\" defaultVersion=\"32.0\">\n        <version number=\"32.0\">\n            <region name=\"first\">\n                <host name=\"localhost\" port=\"8081\" count=\"1\"/>\n                <host name=\"localhost\" port=\"8082\" count=\"2\"/>\n            </region>\n            <region name=\"second\">\n                <host name=\"localhost\" port=\"8083\" count=\"5\"/>\n            </region>\n        </version>\n    </browser>\n</qa:browsers>\n"
  },
  {
    "path": "testing/group_vars/all.yml",
    "content": "workspace: \"{{ ansible_env.PWD }}/target\""
  },
  {
    "path": "testing/ping-local-gridrouter.sh",
    "content": "#!/usr/bin/env bash\ncurl http://boot2docker:$(docker ps | grep jetty | sed 's/.*0.0.0.0://' | sed 's/->.*//')/ping && echo\n"
  },
  {
    "path": "testing/roles/start/files/gridrouter/conf/application.properties",
    "content": "# suppress inspection \"UnusedProperty\" for whole file\ngrid.config.quota.directory=classpath:quota\ngrid.config.quota.hotReload=true\ngrid.router.evict.sessions.cron=0 * * * * *\ngrid.router.evict.sessions.timeout.seconds=120\n"
  },
  {
    "path": "testing/roles/start/files/gridrouter/conf/quota/selenium.xml",
    "content": "<browsers>\n    <browser name=\"firefox\" defaultVersion=\"38.0\">\n        <version number=\"38.0\">\n            <region name=\"first\">\n                <host name=\"firefox\" port=\"4444\" count=\"5\"/>\n            </region>\n        </version>\n    </browser>\n    <browser name=\"chrome\" defaultVersion=\"43.0\">\n        <version number=\"43.0\">\n            <region name=\"first\">\n                <host name=\"chrome\" port=\"4444\" count=\"5\"/>\n            </region>\n        </version>\n    </browser>\n</browsers>\n"
  },
  {
    "path": "testing/roles/start/files/gridrouter/conf/users.properties",
    "content": "selenium:selenium, user"
  },
  {
    "path": "testing/roles/start/files/gridrouter/webapps/ROOT.xml",
    "content": "<?xml version=\"1.0\"  encoding=\"UTF-8\"?>\n<!DOCTYPE Configure PUBLIC\n        \"-//Mort Bay Consulting//DTD Configure//EN\"\n        \"http://www.eclipse.org/jetty/configure_9_0.dtd\">\n\n<Configure class=\"org.eclipse.jetty.webapp.WebAppContext\">\n    <Set name=\"contextPath\">/</Set>\n    <Set name=\"war\">/etc/gridrouter/war/ROOT.war</Set>\n\n    <Set name=\"extraClasspath\">/etc/gridrouter/conf/</Set>\n\n    <Get name=\"securityHandler\">\n        <Set name=\"loginService\">\n            <New class=\"org.eclipse.jetty.security.HashLoginService\">\n                <Set name=\"name\">Selenium Grid Router</Set>\n                <Set name=\"config\">/etc/gridrouter/conf/users.properties</Set>\n                <Call name=\"start\"/>\n            </New>\n        </Set>\n    </Get>\n</Configure>"
  },
  {
    "path": "testing/roles/start/tasks/before.yml",
    "content": "- debug: msg=\"workspace {{ workspace }}\""
  },
  {
    "path": "testing/roles/start/tasks/main.yml",
    "content": "---\n  - include: before.yml\n  - include: start-selenium.yml version=\"2.46.0\" browser=\"firefox\"\n  - include: start-selenium.yml version=\"2.46.0\" browser=\"chrome\"\n  - include: start-gridrouter.yml\n\n"
  },
  {
    "path": "testing/roles/start/tasks/start-gridrouter.yml",
    "content": "- name: copy configuration files\n  copy: src=gridrouter dest={{ workspace }}\n\n- shell: \"ls -d {{ ansible_env.PWD }}/../proxy/target/*.war\"\n  register: war_path\n\n- file: path={{ war_path.stdout }} state=file\n- file: path={{ workspace }}/gridrouter/war/ state=directory\n- copy: src={{ war_path.stdout }} dest={{ workspace }}/gridrouter/war/ROOT.war\n\n- name: start jetty with gridrouter\n  docker:\n    name: gridrouter\n    image: jetty:9.3.2\n    expose:\n      - \"8080\"\n    ports:\n      - \"8080\"\n    links:\n      - \"chrome\"\n      - \"firefox\"\n    volumes:\n      - \"{{ workspace }}/gridrouter/webapps:/var/lib/jetty/webapps\"\n      - \"{{ workspace }}/gridrouter/conf:/etc/gridrouter/conf\"\n      - \"{{ workspace }}/gridrouter/war:/etc/gridrouter/war\"\n    state: started"
  },
  {
    "path": "testing/roles/start/tasks/start-selenium.yml",
    "content": "- name: start selenium standalone with {{ browser }}\n  docker:\n    name: \"{{ browser }}\"\n    image: selenium/standalone-{{ browser }}:{{ version }}\n    expose:\n      - \"4444\"\n    state: started"
  },
  {
    "path": "testing/roles/stop/tasks/before.yml",
    "content": "- debug: msg=\"workspace {{ workspace }}\""
  },
  {
    "path": "testing/roles/stop/tasks/main.yml",
    "content": "---\n  - include: before.yml\n  - include: stop-selenium.yml version=\"2.46.0\" browser=\"firefox\"\n  - include: stop-selenium.yml version=\"2.46.0\" browser=\"chrome\"\n  - include: stop-gridrouter.yml"
  },
  {
    "path": "testing/roles/stop/tasks/stop-gridrouter.yml",
    "content": "- name: stop jetty with gridrouter\n  docker:\n    name: gridrouter\n    image: jetty:9.3.0-jre8\n    state: absent\n\n- name: delete workspace\n  file: path={{ workspace }} state=absent\n  ignore_errors: yes"
  },
  {
    "path": "testing/roles/stop/tasks/stop-selenium.yml",
    "content": "- name: stop selenium standalone with {{ browser }}\n  docker:\n    name: \"{{ browser }}\"\n    image: selenium/standalone-{{ browser }}:{{ version }}\n    expose:\n      - \"4444\"\n    state: absent"
  },
  {
    "path": "testing/roles/test/files/java/pom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n    <modelVersion>4.0.0</modelVersion>\n\n    <groupId>ru.qatools.seleniumkit</groupId>\n    <artifactId>gridrouter-e2e-java</artifactId>\n    <version>1.0-SNAPSHOT</version>\n\n    <dependencies>\n        <dependency>\n            <groupId>junit</groupId>\n            <artifactId>junit</artifactId>\n            <version>4.11</version>\n            <scope>test</scope>\n        </dependency>\n        <dependency>\n            <groupId>org.seleniumhq.selenium</groupId>\n            <artifactId>selenium-java</artifactId>\n            <version>2.46.0</version>\n            <scope>test</scope>\n        </dependency>\n    </dependencies>\n\n</project>"
  },
  {
    "path": "testing/roles/test/files/java/run.sh",
    "content": "#! /bin/sh\nmvn -f /code/pom.xml test"
  },
  {
    "path": "testing/roles/test/files/java/src/test/java/SeleniumTest.java",
    "content": "import org.junit.Test;\nimport org.openqa.selenium.WebDriver;\nimport org.openqa.selenium.remote.DesiredCapabilities;\nimport org.openqa.selenium.remote.RemoteWebDriver;\n\nimport java.net.URL;\n\nimport static org.hamcrest.CoreMatchers.equalTo;\nimport static org.junit.Assert.assertThat;\n\npublic class SeleniumTest {\n\n    @Test\n    public void testConnection() throws Exception {\n        URL url = new URL(\"http://selenium:selenium@gridrouter:8080/wd/hub\");\n        DesiredCapabilities capabilities = DesiredCapabilities.firefox();\n        capabilities.setVersion(\"38.0\");\n        WebDriver driver = new RemoteWebDriver(url, capabilities);\n        driver.get(\"http://www.yandex.ru\");\n        assertThat(driver.getTitle(), equalTo(\"Яндекс\"));\n    }\n}\n"
  },
  {
    "path": "testing/roles/test/files/js/config.json",
    "content": "{\n\t\"baseUrl\": \"http://selenium:selenium@gridrouter:8080/wd/hub\"\n}\n"
  },
  {
    "path": "testing/roles/test/files/js/fixtures/big-script.js",
    "content": "function prepareScreenshotUnsafe(selectors, opts) {\n    var rect = getCaptureRect(selectors);\n    if (rect.error) {\n        return rect;\n    }\n\n    var viewportHeight = window.innerHeight || document.documentElement.clientHeight,\n        viewportWidth = window.innerWidth || document.documentElement.clientWidth,\n        documentHeight = document.documentElement.scrollHeight,\n        documentWidth = document.documentElement.scrollWidth,\n        coverage,\n        viewPort = new Rect({\n            left: util.getScrollLeft(),\n            top: util.getScrollTop(),\n            width: viewportWidth,\n            height: viewportHeight\n        });\n\n    if (!viewPort.rectInside(rect)) {\n        window.scrollTo(rect.left, rect.top);\n    }\n\n    if (opts.coverage) {\n        coverage = require('./gemini.coverage').collectCoverage(rect);\n    }\n\n    return {\n        viewportOffset: {\n            top: util.getScrollTop(),\n            left: util.getScrollLeft()\n        },\n        captureArea: rect.serialize(),\n        ignoreAreas: findIgnoreAreas(opts.ignoreSelectors),\n        viewportHeight: Math.round(viewportHeight),\n        documentHeight: Math.round(documentHeight),\n        documentWidth: Math.round(documentWidth),\n        coverage: coverage,\n        canHaveCaret: isEditable(document.activeElement)\n    };\n}\n\n\nfunction getElementCaptureRect(element) {\n    var pseudo = [':before', ':after'],\n        css = window.getComputedStyle(element),\n        clientRect = rect.getAbsoluteClientRect(element);\n\n    if (isHidden(css, clientRect)) {\n        return null;\n    }\n\n    var elementRect = getExtRect(css, clientRect);\n\n    util.each(pseudo, function(pseudoEl) {\n        css = window.getComputedStyle(element, pseudoEl);\n        elementRect = elementRect.merge(getExtRect(css, clientRect));\n    });\n\n    return elementRect;\n}\n\nfunction getExtRect(css, clientRect) {\n    var shadows = parseBoxShadow(css.boxShadow),\n        outline = parseInt(css.outlineWidth, 10);\n\n    if (isNaN(outline)) {\n        outline = 0;\n    }\n\n    return adjustRect(clientRect, shadows, outline);\n}\n\nfunction parseBoxShadow(value) {\n    value = value || '';\n    var regex = /[-+]?\\d*\\.?\\d+px/g,\n        values = value.split(','),\n        results = [],\n        match;\n\n    util.each(values, function(value) {\n        if ((match = value.match(regex))) {\n            results.push({\n                offsetX: parseFloat(match[0]),\n                offsetY: parseFloat(match[1]) || 0,\n                blurRadius: parseFloat(match[2]) || 0,\n                spreadRadius: parseFloat(match[3]) || 0,\n                inset: value.indexOf('inset') !== -1\n            });\n        }\n    });\n    return results;\n}\n\nfunction adjustRect(rect, shadows, outline) {\n    var shadowRect = calculateShadowRect(rect, shadows),\n        outlineRect = calculateOutlineRect(rect, outline);\n    return shadowRect.merge(outlineRect);\n}\n\nfunction calculateOutlineRect(rect, outline) {\n    return new Rect({\n        top: Math.max(0, rect.top - outline),\n        left: Math.max(0, rect.left - outline),\n        bottom: rect.bottom + outline,\n        right: rect.right + outline\n    });\n}\n\nfunction calculateShadowRect(rect, shadows) {\n    var extent = calculateShadowExtent(shadows);\n    return new Rect({\n        left: Math.max(0, rect.left + extent.left),\n        top: Math.max(0, rect.top + extent.top),\n        width: rect.width - extent.left + extent.right,\n        height: rect.height - extent.top + extent.bottom\n    });\n}\n\nfunction calculateShadowExtent(shadows) {\n    var result = {top: 0, left: 0, right: 0, bottom: 0};\n\n    util.each(shadows, function(shadow) {\n        if (shadow.inset) {\n            //skip inset shadows\n            return;\n        }\n\n        var blurAndSpread = shadow.spreadRadius + shadow.blurRadius;\n        result.left = Math.min(shadow.offsetX - blurAndSpread, result.left);\n        result.right = Math.max(shadow.offsetX + blurAndSpread, result.right);\n        result.top = Math.min(shadow.offsetY - blurAndSpread, result.top);\n        result.bottom = Math.max(shadow.offsetY + blurAndSpread, result.bottom);\n    });\n    return result;\n}\n\nfunction isEditable(element) {\n    if (!element) {\n        return false;\n    }\n    return /^(input|textarea)$/i.test(element.tagName) ||\n        element.isContentEditable;\n}\nreturn 'ok';\n"
  },
  {
    "path": "testing/roles/test/files/js/package.json",
    "content": "{\n  \"name\": \"wd-test\",\n  \"version\": \"1.0.0\",\n  \"description\": \"webdriver test\",\n  \"main\": \"index.js\",\n  \"directories\": {\n    \"test\": \"test\"\n  },\n  \"devDependencies\": {\n    \"mocha\": \"^2.2.5\",\n    \"wd\": \"^0.3.12\",\n    \"webdriver-http-sync\": \"^1.1.0\"\n  },\n  \"scripts\": {\n    \"test\": \"mocha --reporter xunit --reporter-options output=target/surefire-reports/js-selenium-test.xml\"\n  },\n  \"author\": \"\",\n  \"license\": \"ISC\"\n}\n"
  },
  {
    "path": "testing/roles/test/files/js/run.sh",
    "content": "#! /bin/sh\ncd /code\nnpm install\nnpm test\n"
  },
  {
    "path": "testing/roles/test/files/js/test/selenium-test-sync.js",
    "content": "var assert = require('assert'),\n    config = require('../config.json'),\n    WebDriver = require('webdriver-http-sync');\n\ndescribe('\"webdriver-http-sync\" selenium client for js', function () {\n    it('should work', function () {\n\n        this.timeout(60000);\n\n        var driver = new WebDriver(config.baseUrl, {\n            browserName: 'firefox',\n            version: '38'\n        });\n        driver.navigateTo('http://yandex.ru');\n        var title = driver.getPageTitle();\n        assert.equal(title, 'Яндекс');\n        driver.close();\n    });\n});\n"
  },
  {
    "path": "testing/roles/test/files/js/test/selenium-test-wd.js",
    "content": "'use strict';\nvar fs = require('fs'),\n    assert = require('assert'),\n    config = require('../config.json'),\n    wd = require('wd');\n\ndescribe('\"wd\" selenium client for js', function () {\n    var driver;\n    beforeEach(function() {\n        this.timeout(60000);\n        return driver = wd.promiseChainRemote(config.baseUrl)\n            .init({\n                browserName: 'chrome',\n                version: '43'\n            })\n            .get('http://yandex.ru');\n    });\n\n    it('should work', function () {\n        return driver.title().then(function (title) {\n                assert.equal(title, 'Яндекс');\n            })\n            .fin(function () {\n                return driver.quit();\n            });\n    });\n\n    it('should push and evaluate big scripts', function() {\n        return driver.execute(fs.readFileSync(__dirname + '/../fixtures/big-script.js', 'utf-8')).then(function(result) {\n            assert.equal(result, 'ok');\n        }).fin(function() {\n            return driver.quit();\n        });\n    })\n});\n"
  },
  {
    "path": "testing/roles/test/files/python/requirements.txt",
    "content": "selenium==2.46.0\npytest==2.7.1"
  },
  {
    "path": "testing/roles/test/files/python/run.sh",
    "content": "#! /bin/sh\npip install -r /code/requirements.txt\npy.test --junitxml=/code/target/surefire-reports/test_selenium.xml -q /code/src/test_selenium.py"
  },
  {
    "path": "testing/roles/test/files/python/src/test_selenium.py",
    "content": "from selenium import webdriver\nfrom selenium.webdriver.common.desired_capabilities import DesiredCapabilities\n\nclass TestSelenium:\n    def test_selenium(self):\n        driver = webdriver.Remote(\n            command_executor='http://selenium:selenium@gridrouter:8080/wd/hub',\n            desired_capabilities=DesiredCapabilities.CHROME)\n\n        driver.get('http://www.yandex.ru')\n        assert driver.title != ''\n        driver.close()\n"
  },
  {
    "path": "testing/roles/test/tasks/after.yml",
    "content": "- name: \"copy report files\"\n  copy: src={{ workspace }}/report/ dest={{ ansible_env.PWD }}/target/surefire-reports\n"
  },
  {
    "path": "testing/roles/test/tasks/before.yml",
    "content": "- debug: msg=\"workspace {{ workspace }}\""
  },
  {
    "path": "testing/roles/test/tasks/main.yml",
    "content": "---\n  - include: before.yml\n  - include: run-tests.yml language=\"python\" image=\"python:2.7\"\n  - include: run-tests.yml language=\"java\" image=\"maven:3.3-jdk-8\"\n  - include: after.yml\n"
  },
  {
    "path": "testing/roles/test/tasks/run-tests.yml",
    "content": "- name: \"{{ language }} gathering facts\"\n  set_fact:\n    name: \"{{ language }}_tests\"\n    project: \"{{ workspace }}/{{ language }}\"\n\n- name: \"{{ language }} | copy project files\"\n  copy: src={{ language }} dest={{ workspace }}\n\n- name: \"{{ language }} | grant script execution privileges\"\n  file: path={{ workspace }}/{{ language }}/run.sh mode=u+x\n\n- name: \"{{ language }} | start container\"\n  docker:\n    name: \"{{ name}}\"\n    image: \"{{ image }}\"\n    command: /code/run.sh\n    links:\n      - \"gridrouter\"\n    volumes:\n      - \"{{ project }}:/code\"\n      - \"{{ workspace }}/report:/code/target/surefire-reports\"\n    state: started\n\n- name: \"{{ language }} | wait until tests complete\"\n  command: \"docker wait {{ name }}\"\n  register: tests_result\n\n- name: \"{{ language }} | delete container\"\n  docker:\n    name: \"{{ name}}\"\n    image: \"{{ image }}\"\n    state: absent\n\n- name: \"{{ language }} | delete project\"\n  file: path={{ project }} state=absent\n  ignore_errors: yes\n"
  },
  {
    "path": "testing/start.yml",
    "content": "---\n\n- hosts: 127.0.0.1\n  connection: local\n\n  roles:\n    - start"
  },
  {
    "path": "testing/stop.yml",
    "content": "---\n\n- hosts: 127.0.0.1\n  connection: local\n\n  roles:\n    - stop"
  },
  {
    "path": "testing/test.yml",
    "content": "---\n\n- hosts: 127.0.0.1\n  connection: local\n\n  roles:\n    - test"
  }
]