Browse Source

Merge pull request #18735 from javanna/feature/http_client

Low level Rest Client
Luca Cavanna 9 years ago
parent
commit
f0f4db08e0
81 changed files with 7026 additions and 880 deletions
  1. 2 0
      build.gradle
  2. 1 0
      buildSrc/build.gradle
  3. 1 3
      buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginBuildPlugin.groovy
  4. 3 2
      buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/NamingConventionsTask.groovy
  5. 31 26
      buildSrc/src/main/java/org/elasticsearch/test/NamingConventionsCheck.java
  6. 0 5
      buildSrc/src/main/resources/checkstyle_suppressions.xml
  7. 2 1
      buildSrc/version.properties
  8. 88 0
      client-sniffer/build.gradle
  9. 1 0
      client-sniffer/licenses/commons-codec-1.10.jar.sha1
  10. 202 0
      client-sniffer/licenses/commons-codec-LICENSE.txt
  11. 17 0
      client-sniffer/licenses/commons-codec-NOTICE.txt
  12. 1 0
      client-sniffer/licenses/commons-logging-1.1.3.jar.sha1
  13. 202 0
      client-sniffer/licenses/commons-logging-LICENSE.txt
  14. 6 0
      client-sniffer/licenses/commons-logging-NOTICE.txt
  15. 1 0
      client-sniffer/licenses/httpclient-4.5.2.jar.sha1
  16. 558 0
      client-sniffer/licenses/httpclient-LICENSE.txt
  17. 6 0
      client-sniffer/licenses/httpclient-NOTICE.txt
  18. 1 0
      client-sniffer/licenses/httpcore-4.4.4.jar.sha1
  19. 558 0
      client-sniffer/licenses/httpcore-LICENSE.txt
  20. 6 0
      client-sniffer/licenses/httpcore-NOTICE.txt
  21. 1 0
      client-sniffer/licenses/jackson-core-2.7.1.jar.sha1
  22. 8 0
      client-sniffer/licenses/jackson-core-LICENSE
  23. 20 0
      client-sniffer/licenses/jackson-core-NOTICE
  24. 194 0
      client-sniffer/src/main/java/org/elasticsearch/client/sniff/HostsSniffer.java
  25. 65 0
      client-sniffer/src/main/java/org/elasticsearch/client/sniff/SniffOnFailureListener.java
  26. 206 0
      client-sniffer/src/main/java/org/elasticsearch/client/sniff/Sniffer.java
  27. 69 0
      client-sniffer/src/test/java/org/elasticsearch/client/sniff/HostsSnifferBuilderTests.java
  28. 269 0
      client-sniffer/src/test/java/org/elasticsearch/client/sniff/HostsSnifferTests.java
  29. 13 15
      client-sniffer/src/test/java/org/elasticsearch/client/sniff/MockHostsSniffer.java
  30. 57 0
      client-sniffer/src/test/java/org/elasticsearch/client/sniff/SniffOnFailureListenerTests.java
  31. 84 0
      client-sniffer/src/test/java/org/elasticsearch/client/sniff/SnifferBuilderTests.java
  32. 80 0
      client/build.gradle
  33. 1 0
      client/licenses/commons-codec-1.10.jar.sha1
  34. 202 0
      client/licenses/commons-codec-LICENSE.txt
  35. 17 0
      client/licenses/commons-codec-NOTICE.txt
  36. 1 0
      client/licenses/commons-logging-1.1.3.jar.sha1
  37. 202 0
      client/licenses/commons-logging-LICENSE.txt
  38. 6 0
      client/licenses/commons-logging-NOTICE.txt
  39. 1 0
      client/licenses/httpclient-4.5.2.jar.sha1
  40. 558 0
      client/licenses/httpclient-LICENSE.txt
  41. 6 0
      client/licenses/httpclient-NOTICE.txt
  42. 1 0
      client/licenses/httpcore-4.4.4.jar.sha1
  43. 558 0
      client/licenses/httpcore-LICENSE.txt
  44. 6 0
      client/licenses/httpcore-NOTICE.txt
  45. 71 0
      client/src/main/java/org/elasticsearch/client/DeadHostState.java
  46. 5 4
      client/src/main/java/org/elasticsearch/client/HttpDeleteWithEntity.java
  47. 5 4
      client/src/main/java/org/elasticsearch/client/HttpGetWithEntity.java
  48. 146 0
      client/src/main/java/org/elasticsearch/client/RequestLogger.java
  49. 115 0
      client/src/main/java/org/elasticsearch/client/Response.java
  50. 66 0
      client/src/main/java/org/elasticsearch/client/ResponseException.java
  51. 475 0
      client/src/main/java/org/elasticsearch/client/RestClient.java
  52. 42 0
      client/src/test/java/org/elasticsearch/client/CloseableBasicHttpResponse.java
  53. 145 0
      client/src/test/java/org/elasticsearch/client/RequestLoggerTests.java
  54. 108 0
      client/src/test/java/org/elasticsearch/client/RestClientBuilderTests.java
  55. 218 0
      client/src/test/java/org/elasticsearch/client/RestClientIntegTests.java
  56. 275 0
      client/src/test/java/org/elasticsearch/client/RestClientMultipleHostsTests.java
  57. 421 0
      client/src/test/java/org/elasticsearch/client/RestClientSingleHostTests.java
  58. 84 0
      client/src/test/java/org/elasticsearch/client/RestClientTestUtil.java
  59. 52 0
      client/src/test/java/org/elasticsearch/client/TrackingFailureListener.java
  60. 0 1
      core/src/test/java/org/elasticsearch/ExceptionSerializationTests.java
  61. 38 50
      core/src/test/java/org/elasticsearch/http/netty/NettyHttpCompressionIT.java
  62. 13 15
      core/src/test/java/org/elasticsearch/options/detailederrors/DetailedErrorsDisabledIT.java
  63. 22 24
      core/src/test/java/org/elasticsearch/options/detailederrors/DetailedErrorsEnabledIT.java
  64. 17 10
      core/src/test/java/org/elasticsearch/plugins/ResponseHeaderPluginIT.java
  65. 16 13
      core/src/test/java/org/elasticsearch/rest/CorsNotSetIT.java
  66. 55 43
      core/src/test/java/org/elasticsearch/rest/CorsRegexIT.java
  67. 12 8
      core/src/test/java/org/elasticsearch/rest/action/main/RestMainActionIT.java
  68. 17 25
      core/src/test/java/org/elasticsearch/transport/ContextAndHeaderTransportIT.java
  69. 2 0
      settings.gradle
  70. 3 2
      test/framework/build.gradle
  71. 37 10
      test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java
  72. 0 7
      test/framework/src/main/java/org/elasticsearch/test/hamcrest/ElasticsearchAssertions.java
  73. 0 26
      test/framework/src/main/java/org/elasticsearch/test/hamcrest/ElasticsearchMatchers.java
  74. 34 38
      test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java
  75. 18 19
      test/framework/src/main/java/org/elasticsearch/test/rest/RestTestExecutionContext.java
  76. 8 8
      test/framework/src/main/java/org/elasticsearch/test/rest/Stash.java
  77. 137 131
      test/framework/src/main/java/org/elasticsearch/test/rest/client/RestTestClient.java
  78. 39 17
      test/framework/src/main/java/org/elasticsearch/test/rest/client/RestTestResponse.java
  79. 0 250
      test/framework/src/main/java/org/elasticsearch/test/rest/client/http/HttpRequestBuilder.java
  80. 0 108
      test/framework/src/main/java/org/elasticsearch/test/rest/client/http/HttpResponse.java
  81. 18 15
      test/framework/src/main/java/org/elasticsearch/test/rest/section/DoSection.java

+ 2 - 0
build.gradle

@@ -163,6 +163,8 @@ subprojects {
     "org.elasticsearch.gradle:build-tools:${version}": ':build-tools',
     "org.elasticsearch:rest-api-spec:${version}": ':rest-api-spec',
     "org.elasticsearch:elasticsearch:${version}": ':core',
+    "org.elasticsearch:client:${version}": ':client',
+    "org.elasticsearch:client-sniffer:${version}": ':client-sniffer',
     "org.elasticsearch.test:framework:${version}": ':test:framework',
     "org.elasticsearch.distribution.integ-test-zip:elasticsearch:${version}": ':distribution:integ-test-zip',
     "org.elasticsearch.distribution.zip:elasticsearch:${version}": ':distribution:zip',

+ 1 - 0
buildSrc/build.gradle

@@ -97,6 +97,7 @@ dependencies {
   compile 'de.thetaphi:forbiddenapis:2.2'
   compile 'com.bmuschko:gradle-nexus-plugin:2.3.1'
   compile 'org.apache.rat:apache-rat:0.11'
+  compile 'ru.vyarus:gradle-animalsniffer-plugin:1.0.1'
 }
 
 

+ 1 - 3
buildSrc/src/main/groovy/org/elasticsearch/gradle/plugin/PluginBuildPlugin.groovy

@@ -19,7 +19,6 @@
 package org.elasticsearch.gradle.plugin
 
 import nebula.plugin.publishing.maven.MavenBasePublishPlugin
-import nebula.plugin.publishing.maven.MavenManifestPlugin
 import nebula.plugin.publishing.maven.MavenScmPlugin
 import org.elasticsearch.gradle.BuildPlugin
 import org.elasticsearch.gradle.test.RestIntegTestTask
@@ -27,7 +26,6 @@ import org.elasticsearch.gradle.test.RunTask
 import org.gradle.api.Project
 import org.gradle.api.tasks.SourceSet
 import org.gradle.api.tasks.bundling.Zip
-
 /**
  * Encapsulates build configuration for an Elasticsearch plugin.
  */
@@ -56,7 +54,7 @@ public class PluginBuildPlugin extends BuildPlugin {
             }
 
             project.namingConventions {
-                // Plugins decalare extensions of ESIntegTestCase as "Tests" instead of IT.
+                // Plugins declare integration tests as "Tests" instead of IT.
                 skipIntegTestInDisguise = true
             }
         }

+ 3 - 2
buildSrc/src/main/groovy/org/elasticsearch/gradle/precommit/NamingConventionsTask.groovy

@@ -26,7 +26,6 @@ import org.gradle.api.file.FileCollection
 import org.gradle.api.tasks.Input
 import org.gradle.api.tasks.InputFiles
 import org.gradle.api.tasks.OutputFile
-
 /**
  * Runs NamingConventionsCheck on a classpath/directory combo to verify that
  * tests are named according to our conventions so they'll be picked up by
@@ -90,9 +89,11 @@ public class NamingConventionsTask extends LoggedExec {
             doFirst {
                 args('-Djna.nosys=true')
                 args('-cp', (classpath + extraClasspath).asPath, 'org.elasticsearch.test.NamingConventionsCheck')
-                args(testClass, integTestClass)
+                args('--test-class', testClass)
                 if (skipIntegTestInDisguise) {
                     args('--skip-integ-tests-in-disguise')
+                } else {
+                    args('--integ-test-class', integTestClass)
                 }
                 /*
                  * The test framework has classes that fail the checks to validate that the checks fail properly.

+ 31 - 26
buildSrc/src/main/java/org/elasticsearch/test/NamingConventionsCheck.java

@@ -44,31 +44,36 @@ import java.util.Set;
  */
 public class NamingConventionsCheck {
     public static void main(String[] args) throws IOException {
-        int i = 0;
-        NamingConventionsCheck check = new NamingConventionsCheck(
-                loadClassWithoutInitializing(args[i++]),
-                loadClassWithoutInitializing(args[i++]));
+        Class<?> testClass = null;
+        Class<?> integTestClass = null;
+        Path rootPath = null;
         boolean skipIntegTestsInDisguise = false;
         boolean selfTest = false;
-        while (true) {
-            switch (args[i]) {
-            case "--skip-integ-tests-in-disguise":
-                skipIntegTestsInDisguise = true;
-                i++;
-                continue;
-            case "--self-test":
-                selfTest = true;
-                i++;
-                continue;
-            case "--":
-                i++;
-                break;
-            default:
-                fail("Expected -- before a path.");
+        for (int i = 0; i < args.length; i++) {
+            String arg = args[i];
+            switch (arg) {
+                case "--test-class":
+                    testClass = loadClassWithoutInitializing(args[++i]);
+                    break;
+                case "--integ-test-class":
+                    integTestClass = loadClassWithoutInitializing(args[++i]);
+                    break;
+                case "--skip-integ-tests-in-disguise":
+                    skipIntegTestsInDisguise = true;
+                    break;
+                case "--self-test":
+                    selfTest = true;
+                    break;
+                case "--":
+                    rootPath = Paths.get(args[++i]);
+                    break;
+                default:
+                    fail("unsupported argument '" + arg + "'");
             }
-            break;
         }
-        check.check(Paths.get(args[i]));
+
+        NamingConventionsCheck check = new NamingConventionsCheck(testClass, integTestClass);
+        check.check(rootPath, skipIntegTestsInDisguise);
 
         if (selfTest) {
             assertViolation("WrongName", check.missingSuffix);
@@ -87,9 +92,9 @@ public class NamingConventionsCheck {
         assertNoViolations("Found inner classes that are tests, which are excluded from the test runner", check.innerClasses);
         assertNoViolations("Pure Unit-Test found must subclass [" + check.testClass.getSimpleName() + "]", check.pureUnitTest);
         assertNoViolations("Classes ending with [Tests] must subclass [" + check.testClass.getSimpleName() + "]", check.notImplementing);
-        if (!skipIntegTestsInDisguise) {
-            assertNoViolations("Subclasses of ESIntegTestCase should end with IT as they are integration tests",
-                    check.integTestsInDisguise);
+        if (skipIntegTestsInDisguise == false) {
+            assertNoViolations("Subclasses of " + check.integTestClass.getSimpleName() +
+                    " should end with IT as they are integration tests", check.integTestsInDisguise);
         }
     }
 
@@ -108,7 +113,7 @@ public class NamingConventionsCheck {
         this.integTestClass = integTestClass;
     }
 
-    public void check(Path rootPath) throws IOException {
+    public void check(Path rootPath, boolean skipTestsInDisguised) throws IOException {
         Files.walkFileTree(rootPath, new FileVisitor<Path>() {
             /**
              * The package name of the directory we are currently visiting. Kept as a string rather than something fancy because we load
@@ -143,7 +148,7 @@ public class NamingConventionsCheck {
                     String className = filename.substring(0, filename.length() - ".class".length());
                     Class<?> clazz = loadClassWithoutInitializing(packageName + className);
                     if (clazz.getName().endsWith("Tests")) {
-                        if (integTestClass.isAssignableFrom(clazz)) {
+                        if (skipTestsInDisguised == false && integTestClass.isAssignableFrom(clazz)) {
                             integTestsInDisguise.add(clazz);
                         }
                         if (Modifier.isAbstract(clazz.getModifiers()) || Modifier.isInterface(clazz.getModifiers())) {

+ 0 - 5
buildSrc/src/main/resources/checkstyle_suppressions.xml

@@ -1024,7 +1024,6 @@
   <suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]monitor[/\\]jvm[/\\]JvmGcMonitorServiceSettingsTests.java" checks="LineLength" />
   <suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]monitor[/\\]os[/\\]OsProbeTests.java" checks="LineLength" />
   <suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]nodesinfo[/\\]NodeInfoStreamingTests.java" checks="LineLength" />
-  <suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]options[/\\]detailederrors[/\\]DetailedErrorsEnabledIT.java" checks="LineLength" />
   <suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]plugins[/\\]PluginInfoTests.java" checks="LineLength" />
   <suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]plugins[/\\]PluginsServiceTests.java" checks="LineLength" />
   <suppress files="core[/\\]src[/\\]test[/\\]java[/\\]org[/\\]elasticsearch[/\\]recovery[/\\]FullRollingRestartIT.java" checks="LineLength" />
@@ -1273,9 +1272,6 @@
   <suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]engine[/\\]MockEngineSupport.java" checks="LineLength" />
   <suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]hamcrest[/\\]ElasticsearchAssertions.java" checks="LineLength" />
   <suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]junit[/\\]listeners[/\\]LoggingListener.java" checks="LineLength" />
-  <suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]ESRestTestCase.java" checks="LineLength" />
-  <suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]RestTestExecutionContext.java" checks="LineLength" />
-  <suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]client[/\\]http[/\\]HttpRequestBuilder.java" checks="LineLength" />
   <suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]json[/\\]JsonPath.java" checks="LineLength" />
   <suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]parser[/\\]GreaterThanEqualToParser.java" checks="LineLength" />
   <suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]parser[/\\]GreaterThanParser.java" checks="LineLength" />
@@ -1283,7 +1279,6 @@
   <suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]parser[/\\]LessThanParser.java" checks="LineLength" />
   <suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]parser[/\\]RestTestSuiteParseContext.java" checks="LineLength" />
   <suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]parser[/\\]RestTestSuiteParser.java" checks="LineLength" />
-  <suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]section[/\\]DoSection.java" checks="LineLength" />
   <suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]section[/\\]GreaterThanAssertion.java" checks="LineLength" />
   <suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]section[/\\]GreaterThanEqualToAssertion.java" checks="LineLength" />
   <suppress files="test[/\\]framework[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]test[/\\]rest[/\\]section[/\\]LengthAssertion.java" checks="LineLength" />

+ 2 - 1
buildSrc/version.properties

@@ -17,6 +17,7 @@ httpclient        = 4.5.2
 httpcore          = 4.4.4
 commonslogging    = 1.1.3
 commonscodec      = 1.10
-
+hamcrest          = 1.3
+securemock        = 1.2
 # benchmark dependencies
 jmh               = 1.12

+ 88 - 0
client-sniffer/build.gradle

@@ -0,0 +1,88 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.elasticsearch.gradle.precommit.PrecommitTasks
+import org.gradle.api.JavaVersion
+
+apply plugin: 'elasticsearch.build'
+apply plugin: 'ru.vyarus.animalsniffer'
+
+targetCompatibility = JavaVersion.VERSION_1_7
+sourceCompatibility = JavaVersion.VERSION_1_7
+
+dependencies {
+  compile "org.elasticsearch:client:${version}"
+  compile "org.apache.httpcomponents:httpclient:${versions.httpclient}"
+  compile "org.apache.httpcomponents:httpcore:${versions.httpcore}"
+  compile "commons-codec:commons-codec:${versions.commonscodec}"
+  compile "commons-logging:commons-logging:${versions.commonslogging}"
+  compile "com.fasterxml.jackson.core:jackson-core:${versions.jackson}"
+
+  testCompile "com.carrotsearch.randomizedtesting:randomizedtesting-runner:${versions.randomizedrunner}"
+  testCompile "junit:junit:${versions.junit}"
+  testCompile "org.hamcrest:hamcrest-all:${versions.hamcrest}"
+  //we use the last lucene-test version that is compatible with java 1.7
+  testCompile "org.apache.lucene:lucene-test-framework:5.5.1"
+  testCompile "org.apache.lucene:lucene-core:5.5.1"
+  testCompile "org.apache.lucene:lucene-codecs:5.5.1"
+  testCompile "org.elasticsearch:securemock:${versions.securemock}"
+  testCompile "org.codehaus.mojo:animal-sniffer-annotations:1.15"
+  signature "org.codehaus.mojo.signature:java17:1.0@signature"
+}
+
+forbiddenApisMain {
+  //client does not depend on core, so only jdk signatures should be checked
+  signaturesURLs = [PrecommitTasks.getResource('/forbidden/jdk-signatures.txt')]
+}
+
+forbiddenApisTest {
+  //we are using jdk-internal instead of jdk-non-portable to allow for com.sun.net.httpserver.* usage
+  bundledSignatures -= 'jdk-non-portable'
+  bundledSignatures += 'jdk-internal'
+  //client does not depend on core, so only jdk signatures should be checked
+  signaturesURLs = [PrecommitTasks.getResource('/forbidden/jdk-signatures.txt')]
+}
+
+//JarHell is part of es core, which we don't want to pull in
+jarHell.enabled=false
+
+namingConventions {
+  //we don't have integration tests
+  skipIntegTestInDisguise = true
+}
+
+dependencyLicenses {
+  dependencies = project.configurations.runtime.fileCollection {
+    it.group.startsWith('org.elasticsearch') == false
+  }
+}
+
+thirdPartyAudit.excludes = [
+  //commons-logging optional dependencies
+  'org.apache.avalon.framework.logger.Logger',
+  'org.apache.log.Hierarchy',
+  'org.apache.log.Logger',
+  'org.apache.log4j.Category',
+  'org.apache.log4j.Level',
+  'org.apache.log4j.Logger',
+  'org.apache.log4j.Priority',
+  //commons-logging provided dependencies
+  'javax.servlet.ServletContextEvent',
+  'javax.servlet.ServletContextListener'
+]

+ 1 - 0
client-sniffer/licenses/commons-codec-1.10.jar.sha1

@@ -0,0 +1 @@
+4b95f4897fa13f2cd904aee711aeafc0c5295cd8

+ 202 - 0
client-sniffer/licenses/commons-codec-LICENSE.txt

@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 17 - 0
client-sniffer/licenses/commons-codec-NOTICE.txt

@@ -0,0 +1,17 @@
+Apache Commons Codec
+Copyright 2002-2014 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
+
+src/test/org/apache/commons/codec/language/DoubleMetaphoneTest.java
+contains test data from http://aspell.net/test/orig/batch0.tab.
+Copyright (C) 2002 Kevin Atkinson (kevina@gnu.org)
+
+===============================================================================
+
+The content of package org.apache.commons.codec.language.bm has been translated
+from the original php source code available at http://stevemorse.org/phoneticinfo.htm
+with permission from the original authors.
+Original source copyright:
+Copyright (c) 2008 Alexander Beider & Stephen P. Morse.

+ 1 - 0
client-sniffer/licenses/commons-logging-1.1.3.jar.sha1

@@ -0,0 +1 @@
+f6f66e966c70a83ffbdb6f17a0919eaf7c8aca7f

+ 202 - 0
client-sniffer/licenses/commons-logging-LICENSE.txt

@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 6 - 0
client-sniffer/licenses/commons-logging-NOTICE.txt

@@ -0,0 +1,6 @@
+Apache Commons Logging
+Copyright 2003-2014 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
+

+ 1 - 0
client-sniffer/licenses/httpclient-4.5.2.jar.sha1

@@ -0,0 +1 @@
+733db77aa8d9b2d68015189df76ab06304406e50

+ 558 - 0
client-sniffer/licenses/httpclient-LICENSE.txt

@@ -0,0 +1,558 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+=========================================================================
+
+This project includes Public Suffix List copied from
+<https://publicsuffix.org/list/effective_tld_names.dat>
+licensed under the terms of the Mozilla Public License, v. 2.0
+
+Full license text: <http://mozilla.org/MPL/2.0/>
+
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+    means each individual or legal entity that creates, contributes to
+    the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+    means the combination of the Contributions of others (if any) used
+    by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+    means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+    means Source Code Form to which the initial Contributor has attached
+    the notice in Exhibit A, the Executable Form of such Source Code
+    Form, and Modifications of such Source Code Form, in each case
+    including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+    means
+
+    (a) that the initial Contributor has attached the notice described
+        in Exhibit B to the Covered Software; or
+
+    (b) that the Covered Software was made available under the terms of
+        version 1.1 or earlier of the License, but not also under the
+        terms of a Secondary License.
+
+1.6. "Executable Form"
+    means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+    means a work that combines Covered Software with other material, in
+    a separate file or files, that is not Covered Software.
+
+1.8. "License"
+    means this document.
+
+1.9. "Licensable"
+    means having the right to grant, to the maximum extent possible,
+    whether at the time of the initial grant or subsequently, any and
+    all of the rights conveyed by this License.
+
+1.10. "Modifications"
+    means any of the following:
+
+    (a) any file in Source Code Form that results from an addition to,
+        deletion from, or modification of the contents of Covered
+        Software; or
+
+    (b) any new file in Source Code Form that contains any Covered
+        Software.
+
+1.11. "Patent Claims" of a Contributor
+    means any patent claim(s), including without limitation, method,
+    process, and apparatus claims, in any patent Licensable by such
+    Contributor that would be infringed, but for the grant of the
+    License, by the making, using, selling, offering for sale, having
+    made, import, or transfer of either its Contributions or its
+    Contributor Version.
+
+1.12. "Secondary License"
+    means either the GNU General Public License, Version 2.0, the GNU
+    Lesser General Public License, Version 2.1, the GNU Affero General
+    Public License, Version 3.0, or any later versions of those
+    licenses.
+
+1.13. "Source Code Form"
+    means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+    means an individual or a legal entity exercising rights under this
+    License. For legal entities, "You" includes any entity that
+    controls, is controlled by, or is under common control with You. For
+    purposes of this definition, "control" means (a) the power, direct
+    or indirect, to cause the direction or management of such entity,
+    whether by contract or otherwise, or (b) ownership of more than
+    fifty percent (50%) of the outstanding shares or beneficial
+    ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+    Licensable by such Contributor to use, reproduce, make available,
+    modify, display, perform, distribute, and otherwise exploit its
+    Contributions, either on an unmodified basis, with Modifications, or
+    as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+    for sale, have made, import, and otherwise transfer either its
+    Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+    or
+
+(b) for infringements caused by: (i) Your and any other third party's
+    modifications of Covered Software, or (ii) the combination of its
+    Contributions with other software (except as part of its Contributor
+    Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+    its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+    Form, as described in Section 3.1, and You must inform recipients of
+    the Executable Form how they can obtain a copy of such Source Code
+    Form by reasonable means in a timely manner, at a charge no more
+    than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+    License, or sublicense it under different terms, provided that the
+    license for the Executable Form does not attempt to limit or alter
+    the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+*                                                                      *
+*  6. Disclaimer of Warranty                                           *
+*  -------------------------                                           *
+*                                                                      *
+*  Covered Software is provided under this License on an "as is"       *
+*  basis, without warranty of any kind, either expressed, implied, or  *
+*  statutory, including, without limitation, warranties that the       *
+*  Covered Software is free of defects, merchantable, fit for a        *
+*  particular purpose or non-infringing. The entire risk as to the     *
+*  quality and performance of the Covered Software is with You.        *
+*  Should any Covered Software prove defective in any respect, You     *
+*  (not any Contributor) assume the cost of any necessary servicing,   *
+*  repair, or correction. This disclaimer of warranty constitutes an   *
+*  essential part of this License. No use of any Covered Software is   *
+*  authorized under this License except under this disclaimer.         *
+*                                                                      *
+************************************************************************
+
+************************************************************************
+*                                                                      *
+*  7. Limitation of Liability                                          *
+*  --------------------------                                          *
+*                                                                      *
+*  Under no circumstances and under no legal theory, whether tort      *
+*  (including negligence), contract, or otherwise, shall any           *
+*  Contributor, or anyone who distributes Covered Software as          *
+*  permitted above, be liable to You for any direct, indirect,         *
+*  special, incidental, or consequential damages of any character      *
+*  including, without limitation, damages for lost profits, loss of    *
+*  goodwill, work stoppage, computer failure or malfunction, or any    *
+*  and all other commercial damages or losses, even if such party      *
+*  shall have been informed of the possibility of such damages. This   *
+*  limitation of liability shall not apply to liability for death or   *
+*  personal injury resulting from such party's negligence to the       *
+*  extent applicable law prohibits such limitation. Some               *
+*  jurisdictions do not allow the exclusion or limitation of           *
+*  incidental or consequential damages, so this exclusion and          *
+*  limitation may not apply to You.                                    *
+*                                                                      *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+  This Source Code Form is subject to the terms of the Mozilla Public
+  License, v. 2.0. If a copy of the MPL was not distributed with this
+  file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+  This Source Code Form is "Incompatible With Secondary Licenses", as
+  defined by the Mozilla Public License, v. 2.0.

+ 6 - 0
client-sniffer/licenses/httpclient-NOTICE.txt

@@ -0,0 +1,6 @@
+Apache HttpComponents Client
+Copyright 1999-2016 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
+

+ 1 - 0
client-sniffer/licenses/httpcore-4.4.4.jar.sha1

@@ -0,0 +1 @@
+b31526a230871fbe285fbcbe2813f9c0839ae9b0

+ 558 - 0
client-sniffer/licenses/httpcore-LICENSE.txt

@@ -0,0 +1,558 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+=========================================================================
+
+This project includes Public Suffix List copied from
+<https://publicsuffix.org/list/effective_tld_names.dat>
+licensed under the terms of the Mozilla Public License, v. 2.0
+
+Full license text: <http://mozilla.org/MPL/2.0/>
+
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+    means each individual or legal entity that creates, contributes to
+    the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+    means the combination of the Contributions of others (if any) used
+    by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+    means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+    means Source Code Form to which the initial Contributor has attached
+    the notice in Exhibit A, the Executable Form of such Source Code
+    Form, and Modifications of such Source Code Form, in each case
+    including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+    means
+
+    (a) that the initial Contributor has attached the notice described
+        in Exhibit B to the Covered Software; or
+
+    (b) that the Covered Software was made available under the terms of
+        version 1.1 or earlier of the License, but not also under the
+        terms of a Secondary License.
+
+1.6. "Executable Form"
+    means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+    means a work that combines Covered Software with other material, in
+    a separate file or files, that is not Covered Software.
+
+1.8. "License"
+    means this document.
+
+1.9. "Licensable"
+    means having the right to grant, to the maximum extent possible,
+    whether at the time of the initial grant or subsequently, any and
+    all of the rights conveyed by this License.
+
+1.10. "Modifications"
+    means any of the following:
+
+    (a) any file in Source Code Form that results from an addition to,
+        deletion from, or modification of the contents of Covered
+        Software; or
+
+    (b) any new file in Source Code Form that contains any Covered
+        Software.
+
+1.11. "Patent Claims" of a Contributor
+    means any patent claim(s), including without limitation, method,
+    process, and apparatus claims, in any patent Licensable by such
+    Contributor that would be infringed, but for the grant of the
+    License, by the making, using, selling, offering for sale, having
+    made, import, or transfer of either its Contributions or its
+    Contributor Version.
+
+1.12. "Secondary License"
+    means either the GNU General Public License, Version 2.0, the GNU
+    Lesser General Public License, Version 2.1, the GNU Affero General
+    Public License, Version 3.0, or any later versions of those
+    licenses.
+
+1.13. "Source Code Form"
+    means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+    means an individual or a legal entity exercising rights under this
+    License. For legal entities, "You" includes any entity that
+    controls, is controlled by, or is under common control with You. For
+    purposes of this definition, "control" means (a) the power, direct
+    or indirect, to cause the direction or management of such entity,
+    whether by contract or otherwise, or (b) ownership of more than
+    fifty percent (50%) of the outstanding shares or beneficial
+    ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+    Licensable by such Contributor to use, reproduce, make available,
+    modify, display, perform, distribute, and otherwise exploit its
+    Contributions, either on an unmodified basis, with Modifications, or
+    as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+    for sale, have made, import, and otherwise transfer either its
+    Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+    or
+
+(b) for infringements caused by: (i) Your and any other third party's
+    modifications of Covered Software, or (ii) the combination of its
+    Contributions with other software (except as part of its Contributor
+    Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+    its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+    Form, as described in Section 3.1, and You must inform recipients of
+    the Executable Form how they can obtain a copy of such Source Code
+    Form by reasonable means in a timely manner, at a charge no more
+    than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+    License, or sublicense it under different terms, provided that the
+    license for the Executable Form does not attempt to limit or alter
+    the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+*                                                                      *
+*  6. Disclaimer of Warranty                                           *
+*  -------------------------                                           *
+*                                                                      *
+*  Covered Software is provided under this License on an "as is"       *
+*  basis, without warranty of any kind, either expressed, implied, or  *
+*  statutory, including, without limitation, warranties that the       *
+*  Covered Software is free of defects, merchantable, fit for a        *
+*  particular purpose or non-infringing. The entire risk as to the     *
+*  quality and performance of the Covered Software is with You.        *
+*  Should any Covered Software prove defective in any respect, You     *
+*  (not any Contributor) assume the cost of any necessary servicing,   *
+*  repair, or correction. This disclaimer of warranty constitutes an   *
+*  essential part of this License. No use of any Covered Software is   *
+*  authorized under this License except under this disclaimer.         *
+*                                                                      *
+************************************************************************
+
+************************************************************************
+*                                                                      *
+*  7. Limitation of Liability                                          *
+*  --------------------------                                          *
+*                                                                      *
+*  Under no circumstances and under no legal theory, whether tort      *
+*  (including negligence), contract, or otherwise, shall any           *
+*  Contributor, or anyone who distributes Covered Software as          *
+*  permitted above, be liable to You for any direct, indirect,         *
+*  special, incidental, or consequential damages of any character      *
+*  including, without limitation, damages for lost profits, loss of    *
+*  goodwill, work stoppage, computer failure or malfunction, or any    *
+*  and all other commercial damages or losses, even if such party      *
+*  shall have been informed of the possibility of such damages. This   *
+*  limitation of liability shall not apply to liability for death or   *
+*  personal injury resulting from such party's negligence to the       *
+*  extent applicable law prohibits such limitation. Some               *
+*  jurisdictions do not allow the exclusion or limitation of           *
+*  incidental or consequential damages, so this exclusion and          *
+*  limitation may not apply to You.                                    *
+*                                                                      *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+  This Source Code Form is subject to the terms of the Mozilla Public
+  License, v. 2.0. If a copy of the MPL was not distributed with this
+  file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+  This Source Code Form is "Incompatible With Secondary Licenses", as
+  defined by the Mozilla Public License, v. 2.0.

+ 6 - 0
client-sniffer/licenses/httpcore-NOTICE.txt

@@ -0,0 +1,6 @@
+Apache HttpComponents Client
+Copyright 1999-2016 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
+

+ 1 - 0
client-sniffer/licenses/jackson-core-2.7.1.jar.sha1

@@ -0,0 +1 @@
+4127b62db028f981e81caa248953c0899d720f98

+ 8 - 0
client-sniffer/licenses/jackson-core-LICENSE

@@ -0,0 +1,8 @@
+This copy of Jackson JSON processor streaming parser/generator is licensed under the
+Apache (Software) License, version 2.0 ("the License").
+See the License for details about distribution rights, and the
+specific rights regarding derivate works.
+
+You may obtain a copy of the License at:
+
+http://www.apache.org/licenses/LICENSE-2.0

+ 20 - 0
client-sniffer/licenses/jackson-core-NOTICE

@@ -0,0 +1,20 @@
+# Jackson JSON processor
+
+Jackson is a high-performance, Free/Open Source JSON processing library.
+It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has
+been in development since 2007.
+It is currently developed by a community of developers, as well as supported
+commercially by FasterXML.com.
+
+## Licensing
+
+Jackson core and extension components may licensed under different licenses.
+To find the details that apply to this artifact see the accompanying LICENSE file.
+For more information, including possible other licensing options, contact
+FasterXML.com (http://fasterxml.com).
+
+## Credits
+
+A list of contributors may be found from CREDITS file, which is included
+in some artifacts (usually source distributions); but is always available
+from the source code management (SCM) system project uses.

+ 194 - 0
client-sniffer/src/main/java/org/elasticsearch/client/sniff/HostsSniffer.java

@@ -0,0 +1,194 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client.sniff;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpHost;
+import org.elasticsearch.client.Response;
+import org.elasticsearch.client.RestClient;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Class responsible for sniffing the http hosts from elasticsearch through the nodes info api and returning them back.
+ * Compatible with elasticsearch 5.x and 2.x.
+ */
+public class HostsSniffer {
+
+    private static final Log logger = LogFactory.getLog(HostsSniffer.class);
+
+    private final RestClient restClient;
+    private final Map<String, String> sniffRequestParams;
+    private final Scheme scheme;
+    private final JsonFactory jsonFactory = new JsonFactory();
+
+    protected HostsSniffer(RestClient restClient, long sniffRequestTimeoutMillis, Scheme scheme) {
+        this.restClient = restClient;
+        this.sniffRequestParams = Collections.<String, String>singletonMap("timeout", sniffRequestTimeoutMillis + "ms");
+        this.scheme = scheme;
+    }
+
+    /**
+     * Calls the elasticsearch nodes info api, parses the response and returns all the found http hosts
+     */
+    public List<HttpHost> sniffHosts() throws IOException {
+        try (Response response = restClient.performRequest("get", "/_nodes/http", sniffRequestParams, null)) {
+            return readHosts(response.getEntity());
+        }
+    }
+
+    private List<HttpHost> readHosts(HttpEntity entity) throws IOException {
+        try (InputStream inputStream = entity.getContent()) {
+            JsonParser parser = jsonFactory.createParser(inputStream);
+            if (parser.nextToken() != JsonToken.START_OBJECT) {
+                throw new IOException("expected data to start with an object");
+            }
+            List<HttpHost> hosts = new ArrayList<>();
+            while (parser.nextToken() != JsonToken.END_OBJECT) {
+                if (parser.getCurrentToken() == JsonToken.START_OBJECT) {
+                    if ("nodes".equals(parser.getCurrentName())) {
+                        while (parser.nextToken() != JsonToken.END_OBJECT) {
+                            JsonToken token = parser.nextToken();
+                            assert token == JsonToken.START_OBJECT;
+                            String nodeId = parser.getCurrentName();
+                            HttpHost sniffedHost = readHost(nodeId, parser, this.scheme);
+                            if (sniffedHost != null) {
+                                logger.trace("adding node [" + nodeId + "]");
+                                hosts.add(sniffedHost);
+                            }
+                        }
+                    } else {
+                        parser.skipChildren();
+                    }
+                }
+            }
+            return hosts;
+        }
+    }
+
+    private static HttpHost readHost(String nodeId, JsonParser parser, Scheme scheme) throws IOException {
+        HttpHost httpHost = null;
+        String fieldName = null;
+        while (parser.nextToken() != JsonToken.END_OBJECT) {
+            if (parser.getCurrentToken() == JsonToken.FIELD_NAME) {
+                fieldName = parser.getCurrentName();
+            } else if (parser.getCurrentToken() == JsonToken.START_OBJECT) {
+                if ("http".equals(fieldName)) {
+                    while (parser.nextToken() != JsonToken.END_OBJECT) {
+                        if (parser.getCurrentToken() == JsonToken.VALUE_STRING && "publish_address".equals(parser.getCurrentName())) {
+                            URI boundAddressAsURI = URI.create(scheme + "://" + parser.getValueAsString());
+                            httpHost = new HttpHost(boundAddressAsURI.getHost(), boundAddressAsURI.getPort(),
+                                    boundAddressAsURI.getScheme());
+                        } else if (parser.getCurrentToken() == JsonToken.START_OBJECT) {
+                            parser.skipChildren();
+                        }
+                    }
+                } else {
+                    parser.skipChildren();
+                }
+            }
+        }
+        //http section is not present if http is not enabled on the node, ignore such nodes
+        if (httpHost == null) {
+            logger.debug("skipping node [" + nodeId + "] with http disabled");
+            return null;
+        }
+        return httpHost;
+    }
+
+    /**
+     * Returns a new {@link Builder} to help with {@link HostsSniffer} creation.
+     */
+    public static Builder builder(RestClient restClient) {
+        return new Builder(restClient);
+    }
+
+    public enum Scheme {
+        HTTP("http"), HTTPS("https");
+
+        private final String name;
+
+        Scheme(String name) {
+            this.name = name;
+        }
+
+        @Override
+        public String toString() {
+            return name;
+        }
+    }
+
+    /**
+     * HostsSniffer builder. Helps creating a new {@link HostsSniffer}.
+     */
+    public static class Builder {
+        public static final long DEFAULT_SNIFF_REQUEST_TIMEOUT = TimeUnit.SECONDS.toMillis(1);
+
+        private final RestClient restClient;
+        private long sniffRequestTimeoutMillis = DEFAULT_SNIFF_REQUEST_TIMEOUT;
+        private Scheme scheme;
+
+        private Builder(RestClient restClient) {
+            Objects.requireNonNull(restClient, "restClient cannot be null");
+            this.restClient = restClient;
+        }
+
+        /**
+         * Sets the sniff request timeout (in milliseconds) to be passed in as a query string parameter to elasticsearch.
+         * Allows to halt the request without any failure, as only the nodes that have responded within this timeout will be returned.
+         */
+        public Builder setSniffRequestTimeoutMillis(int sniffRequestTimeoutMillis) {
+            if (sniffRequestTimeoutMillis <= 0) {
+                throw new IllegalArgumentException("sniffRequestTimeoutMillis must be greater than 0");
+            }
+            this.sniffRequestTimeoutMillis = sniffRequestTimeoutMillis;
+            return this;
+        }
+
+        /**
+         * Sets the scheme to associate sniffed nodes with (as it is not returned by elasticsearch)
+         */
+        public Builder setScheme(Scheme scheme) {
+            Objects.requireNonNull(scheme, "scheme cannot be null");
+            this.scheme = scheme;
+            return this;
+        }
+
+        /**
+         * Creates a new {@link HostsSniffer} instance given the provided configuration
+         */
+        public HostsSniffer build() {
+            return new HostsSniffer(restClient, sniffRequestTimeoutMillis, scheme);
+        }
+    }
+}

+ 65 - 0
client-sniffer/src/main/java/org/elasticsearch/client/sniff/SniffOnFailureListener.java

@@ -0,0 +1,65 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client.sniff;
+
+import org.apache.http.HttpHost;
+import org.elasticsearch.client.RestClient;
+
+import java.io.IOException;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * {@link org.elasticsearch.client.RestClient.FailureListener} implementation that allows to perform
+ * sniffing on failure. Gets notified whenever a failure happens and uses a {@link Sniffer} instance
+ * to manually reload hosts and sets them back to the {@link RestClient}. The {@link Sniffer} instance
+ * needs to be lazily set through {@link #setSniffer(Sniffer)}.
+ */
+public class SniffOnFailureListener extends RestClient.FailureListener {
+
+    private volatile Sniffer sniffer;
+    private final AtomicBoolean set;
+
+    public SniffOnFailureListener() {
+        this.set = new AtomicBoolean(false);
+    }
+
+    /**
+     * Sets the {@link Sniffer} instance used to perform sniffing
+     * @throws IllegalStateException if the sniffer was already set, as it can only be set once
+     */
+    public void setSniffer(Sniffer sniffer) {
+        Objects.requireNonNull(sniffer, "sniffer must not be null");
+        if (set.compareAndSet(false, true)) {
+            this.sniffer = sniffer;
+        } else {
+            throw new IllegalStateException("sniffer can only be set once");
+        }
+    }
+
+    @Override
+    public void onFailure(HttpHost host) throws IOException {
+        if (sniffer == null) {
+            throw new IllegalStateException("sniffer was not set, unable to sniff on failure");
+        }
+        //re-sniff immediately but take out the node that failed
+        sniffer.sniffOnFailure(host);
+    }
+}

+ 206 - 0
client-sniffer/src/main/java/org/elasticsearch/client/sniff/Sniffer.java

@@ -0,0 +1,206 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client.sniff;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.http.HttpHost;
+import org.elasticsearch.client.RestClient;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Class responsible for sniffing nodes from an elasticsearch cluster and setting them to a provided instance of {@link RestClient}.
+ * Must be created via {@link Builder}, which allows to set all of the different options or rely on defaults.
+ * A background task fetches the nodes through the {@link HostsSniffer} and sets them to the {@link RestClient} instance.
+ * It is possible to perform sniffing on failure by creating a {@link SniffOnFailureListener} and providing it as an argument to
+ * {@link org.elasticsearch.client.RestClient.Builder#setFailureListener(RestClient.FailureListener)}. The Sniffer implementation
+ * needs to be lazily set to the previously created SniffOnFailureListener through {@link SniffOnFailureListener#setSniffer(Sniffer)}.
+ */
+public final class Sniffer implements Closeable {
+
+    private static final Log logger = LogFactory.getLog(Sniffer.class);
+
+    private final Task task;
+
+    private Sniffer(RestClient restClient, HostsSniffer hostsSniffer, long sniffInterval, long sniffAfterFailureDelay) {
+        this.task = new Task(hostsSniffer, restClient, sniffInterval, sniffAfterFailureDelay);
+    }
+
+    /**
+     * Triggers a new sniffing round and explicitly takes out the failed host provided as argument
+     */
+    public void sniffOnFailure(HttpHost failedHost) {
+        this.task.sniffOnFailure(failedHost);
+    }
+
+    @Override
+    public void close() throws IOException {
+        task.shutdown();
+    }
+
+    private static class Task implements Runnable {
+        private final HostsSniffer hostsSniffer;
+        private final RestClient restClient;
+
+        private final long sniffIntervalMillis;
+        private final long sniffAfterFailureDelayMillis;
+        private final ScheduledExecutorService scheduledExecutorService;
+        private final AtomicBoolean running = new AtomicBoolean(false);
+        private ScheduledFuture<?> scheduledFuture;
+
+        private Task(HostsSniffer hostsSniffer, RestClient restClient, long sniffIntervalMillis, long sniffAfterFailureDelayMillis) {
+            this.hostsSniffer = hostsSniffer;
+            this.restClient = restClient;
+            this.sniffIntervalMillis = sniffIntervalMillis;
+            this.sniffAfterFailureDelayMillis = sniffAfterFailureDelayMillis;
+            this.scheduledExecutorService = Executors.newScheduledThreadPool(1);
+            scheduleNextRun(0);
+        }
+
+        synchronized void scheduleNextRun(long delayMillis) {
+            if (scheduledExecutorService.isShutdown() == false) {
+                try {
+                    if (scheduledFuture != null) {
+                        //regardless of when the next sniff is scheduled, cancel it and schedule a new one with updated delay
+                        this.scheduledFuture.cancel(false);
+                    }
+                    logger.debug("scheduling next sniff in " + delayMillis + " ms");
+                    this.scheduledFuture = this.scheduledExecutorService.schedule(this, delayMillis, TimeUnit.MILLISECONDS);
+                } catch(Exception e) {
+                    logger.error("error while scheduling next sniffer task", e);
+                }
+            }
+        }
+
+        @Override
+        public void run() {
+            sniff(null, sniffIntervalMillis);
+        }
+
+        void sniffOnFailure(HttpHost failedHost) {
+            sniff(failedHost, sniffAfterFailureDelayMillis);
+        }
+
+        void sniff(HttpHost excludeHost, long nextSniffDelayMillis) {
+            if (running.compareAndSet(false, true)) {
+                try {
+                    List<HttpHost> sniffedHosts = hostsSniffer.sniffHosts();
+                    logger.debug("sniffed hosts: " + sniffedHosts);
+                    if (excludeHost != null) {
+                        sniffedHosts.remove(excludeHost);
+                    }
+                    if (sniffedHosts.isEmpty()) {
+                        logger.warn("no hosts to set, hosts will be updated at the next sniffing round");
+                    } else {
+                        this.restClient.setHosts(sniffedHosts.toArray(new HttpHost[sniffedHosts.size()]));
+                    }
+                } catch (Exception e) {
+                    logger.error("error while sniffing nodes", e);
+                } finally {
+                    scheduleNextRun(nextSniffDelayMillis);
+                    running.set(false);
+                }
+            }
+        }
+
+        synchronized void shutdown() {
+            scheduledExecutorService.shutdown();
+            try {
+                if (scheduledExecutorService.awaitTermination(1000, TimeUnit.MILLISECONDS)) {
+                    return;
+                }
+                scheduledExecutorService.shutdownNow();
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+        }
+    }
+
+    /**
+     * Returns a new {@link Builder} to help with {@link Sniffer} creation.
+     */
+    public static Builder builder(RestClient restClient, HostsSniffer hostsSniffer) {
+        return new Builder(restClient, hostsSniffer);
+    }
+
+    /**
+     * Sniffer builder. Helps creating a new {@link Sniffer}.
+     */
+    public static final class Builder {
+        public static final long DEFAULT_SNIFF_INTERVAL = TimeUnit.MINUTES.toMillis(5);
+        public static final long DEFAULT_SNIFF_AFTER_FAILURE_DELAY = TimeUnit.MINUTES.toMillis(1);
+
+        private final RestClient restClient;
+        private final HostsSniffer hostsSniffer;
+        private long sniffIntervalMillis = DEFAULT_SNIFF_INTERVAL;
+        private long sniffAfterFailureDelayMillis = DEFAULT_SNIFF_AFTER_FAILURE_DELAY;
+
+        /**
+         * Creates a new builder instance by providing the {@link RestClient} that will be used to communicate with elasticsearch,
+         * and the
+         */
+        private Builder(RestClient restClient, HostsSniffer hostsSniffer) {
+            Objects.requireNonNull(restClient, "restClient cannot be null");
+            this.restClient = restClient;
+            Objects.requireNonNull(hostsSniffer, "hostsSniffer cannot be null");
+            this.hostsSniffer = hostsSniffer;
+        }
+
+        /**
+         * Sets the interval between consecutive ordinary sniff executions in milliseconds. Will be honoured when
+         * sniffOnFailure is disabled or when there are no failures between consecutive sniff executions.
+         * @throws IllegalArgumentException if sniffIntervalMillis is not greater than 0
+         */
+        public Builder setSniffIntervalMillis(int sniffIntervalMillis) {
+            if (sniffIntervalMillis <= 0) {
+                throw new IllegalArgumentException("sniffIntervalMillis must be greater than 0");
+            }
+            this.sniffIntervalMillis = sniffIntervalMillis;
+            return this;
+        }
+
+        /**
+         * Sets the delay of a sniff execution scheduled after a failure (in milliseconds)
+         */
+        public Builder setSniffAfterFailureDelayMillis(int sniffAfterFailureDelayMillis) {
+            if (sniffAfterFailureDelayMillis <= 0) {
+                throw new IllegalArgumentException("sniffAfterFailureDelayMillis must be greater than 0");
+            }
+            this.sniffAfterFailureDelayMillis = sniffAfterFailureDelayMillis;
+            return this;
+        }
+
+        /**
+         * Creates the {@link Sniffer} based on the provided configuration.
+         */
+        public Sniffer build() {
+            return new Sniffer(restClient, hostsSniffer, sniffIntervalMillis, sniffAfterFailureDelayMillis);
+        }
+    }
+}

+ 69 - 0
client-sniffer/src/test/java/org/elasticsearch/client/sniff/HostsSnifferBuilderTests.java

@@ -0,0 +1,69 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client.sniff;
+
+import com.carrotsearch.randomizedtesting.generators.RandomInts;
+import com.carrotsearch.randomizedtesting.generators.RandomPicks;
+import org.apache.http.HttpHost;
+import org.apache.lucene.util.LuceneTestCase;
+import org.elasticsearch.client.RestClient;
+
+public class HostsSnifferBuilderTests extends LuceneTestCase {
+
+    public void testBuild() throws Exception {
+        try {
+            HostsSniffer.builder(null);
+            fail("should have failed");
+        } catch(NullPointerException e) {
+            assertEquals(e.getMessage(), "restClient cannot be null");
+        }
+
+        int numNodes = RandomInts.randomIntBetween(random(), 1, 5);
+        HttpHost[] hosts = new HttpHost[numNodes];
+        for (int i = 0; i < numNodes; i++) {
+            hosts[i] = new HttpHost("localhost", 9200 + i);
+        }
+
+        try (RestClient client = RestClient.builder(hosts).build()) {
+            try {
+                HostsSniffer.builder(client).setScheme(null);
+                fail("should have failed");
+            } catch(NullPointerException e) {
+                assertEquals(e.getMessage(), "scheme cannot be null");
+            }
+
+            try {
+                HostsSniffer.builder(client).setSniffRequestTimeoutMillis(RandomInts.randomIntBetween(random(), Integer.MIN_VALUE, 0));
+                fail("should have failed");
+            } catch(IllegalArgumentException e) {
+                assertEquals(e.getMessage(), "sniffRequestTimeoutMillis must be greater than 0");
+            }
+
+            HostsSniffer.Builder builder = HostsSniffer.builder(client);
+            if (random().nextBoolean()) {
+                builder.setScheme(RandomPicks.randomFrom(random(), HostsSniffer.Scheme.values()));
+            }
+            if (random().nextBoolean()) {
+                builder.setSniffRequestTimeoutMillis(RandomInts.randomIntBetween(random(), 1, Integer.MAX_VALUE));
+            }
+            assertNotNull(builder.build());
+        }
+    }
+}

+ 269 - 0
client-sniffer/src/test/java/org/elasticsearch/client/sniff/HostsSnifferTests.java

@@ -0,0 +1,269 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client.sniff;
+
+import com.carrotsearch.randomizedtesting.generators.RandomInts;
+import com.carrotsearch.randomizedtesting.generators.RandomPicks;
+import com.carrotsearch.randomizedtesting.generators.RandomStrings;
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+import org.apache.http.Consts;
+import org.apache.http.HttpHost;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.lucene.util.LuceneTestCase;
+import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement;
+import org.elasticsearch.client.Response;
+import org.elasticsearch.client.ResponseException;
+import org.elasticsearch.client.RestClient;
+import org.junit.After;
+import org.junit.Before;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.StringWriter;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.equalTo;
+
+//animal-sniffer doesn't like our usage of com.sun.net.httpserver.* classes
+@IgnoreJRERequirement
+public class HostsSnifferTests extends LuceneTestCase {
+
+    private int sniffRequestTimeout;
+    private HostsSniffer.Scheme scheme;
+    private SniffResponse sniffResponse;
+    private HttpServer httpServer;
+
+    @Before
+    public void startHttpServer() throws IOException {
+        this.sniffRequestTimeout = RandomInts.randomIntBetween(random(), 1000, 10000);
+        this.scheme = RandomPicks.randomFrom(random(), HostsSniffer.Scheme.values());
+        if (rarely()) {
+            this.sniffResponse = SniffResponse.buildFailure();
+        } else {
+            this.sniffResponse = buildSniffResponse(scheme);
+        }
+        this.httpServer = createHttpServer(sniffResponse, sniffRequestTimeout);
+        this.httpServer.start();
+    }
+
+    @After
+    public void stopHttpServer() throws IOException {
+        httpServer.stop(0);
+    }
+
+    public void testSniffNodes() throws IOException, URISyntaxException {
+        HttpHost httpHost = new HttpHost(httpServer.getAddress().getHostString(), httpServer.getAddress().getPort());
+        try (RestClient restClient = RestClient.builder(httpHost).build()) {
+            HostsSniffer sniffer = new HostsSniffer(restClient, sniffRequestTimeout, scheme);
+            try {
+                List<HttpHost> sniffedHosts = sniffer.sniffHosts();
+                if (sniffResponse.isFailure) {
+                    fail("sniffNodes should have failed");
+                }
+                assertThat(sniffedHosts.size(), equalTo(sniffResponse.hosts.size()));
+                Iterator<HttpHost> responseHostsIterator = sniffResponse.hosts.iterator();
+                for (HttpHost sniffedHost : sniffedHosts) {
+                    assertEquals(sniffedHost, responseHostsIterator.next());
+                }
+            } catch(ResponseException e) {
+                Response response = e.getResponse();
+                if (sniffResponse.isFailure) {
+                    assertThat(e.getMessage(), containsString("GET " + httpHost + "/_nodes/http?timeout=" + sniffRequestTimeout + "ms"));
+                    assertThat(e.getMessage(), containsString(Integer.toString(sniffResponse.nodesInfoResponseCode)));
+                    assertThat(response.getHost(), equalTo(httpHost));
+                    assertThat(response.getStatusLine().getStatusCode(), equalTo(sniffResponse.nodesInfoResponseCode));
+                    assertThat(response.getRequestLine().toString(),
+                            equalTo("GET /_nodes/http?timeout=" + sniffRequestTimeout + "ms HTTP/1.1"));
+                } else {
+                    fail("sniffNodes should have succeeded: " + response.getStatusLine());
+                }
+            }
+        }
+    }
+
+    private static HttpServer createHttpServer(final SniffResponse sniffResponse, final int sniffTimeoutMillis) throws IOException {
+        HttpServer httpServer = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
+        httpServer.createContext("/_nodes/http", new ResponseHandler(sniffTimeoutMillis, sniffResponse));
+        return httpServer;
+    }
+
+    //animal-sniffer doesn't like our usage of com.sun.net.httpserver.* classes
+    @IgnoreJRERequirement
+    private static class ResponseHandler implements HttpHandler {
+        private final int sniffTimeoutMillis;
+        private final SniffResponse sniffResponse;
+
+        ResponseHandler(int sniffTimeoutMillis, SniffResponse sniffResponse) {
+            this.sniffTimeoutMillis = sniffTimeoutMillis;
+            this.sniffResponse = sniffResponse;
+        }
+
+        @Override
+        public void handle(HttpExchange httpExchange) throws IOException {
+            if (httpExchange.getRequestMethod().equals(HttpGet.METHOD_NAME)) {
+                if (httpExchange.getRequestURI().getRawQuery().equals("timeout=" + sniffTimeoutMillis + "ms")) {
+                    String nodesInfoBody = sniffResponse.nodesInfoBody;
+                    httpExchange.sendResponseHeaders(sniffResponse.nodesInfoResponseCode, nodesInfoBody.length());
+                    try (OutputStream out = httpExchange.getResponseBody()) {
+                        out.write(nodesInfoBody.getBytes(Consts.UTF_8));
+                        return;
+                    }
+                }
+            }
+            httpExchange.sendResponseHeaders(404, 0);
+            httpExchange.close();
+        }
+    }
+
+    private static SniffResponse buildSniffResponse(HostsSniffer.Scheme scheme) throws IOException {
+        int numNodes = RandomInts.randomIntBetween(random(), 1, 5);
+        List<HttpHost> hosts = new ArrayList<>(numNodes);
+        JsonFactory jsonFactory = new JsonFactory();
+        StringWriter writer = new StringWriter();
+        JsonGenerator generator = jsonFactory.createGenerator(writer);
+        generator.writeStartObject();
+        if (random().nextBoolean()) {
+            generator.writeStringField("cluster_name", "elasticsearch");
+        }
+        if (random().nextBoolean()) {
+            generator.writeObjectFieldStart("bogus_object");
+            generator.writeEndObject();
+        }
+        generator.writeObjectFieldStart("nodes");
+        for (int i = 0; i < numNodes; i++) {
+            String nodeId = RandomStrings.randomAsciiOfLengthBetween(random(), 5, 10);
+            generator.writeObjectFieldStart(nodeId);
+            if (random().nextBoolean()) {
+                generator.writeObjectFieldStart("bogus_object");
+                generator.writeEndObject();
+            }
+            if (random().nextBoolean()) {
+                generator.writeArrayFieldStart("bogus_array");
+                generator.writeStartObject();
+                generator.writeEndObject();
+                generator.writeEndArray();
+            }
+            boolean isHttpEnabled = rarely() == false;
+            if (isHttpEnabled) {
+                String host = "host" + i;
+                int port = RandomInts.randomIntBetween(random(), 9200, 9299);
+                HttpHost httpHost = new HttpHost(host, port, scheme.toString());
+                hosts.add(httpHost);
+                generator.writeObjectFieldStart("http");
+                if (random().nextBoolean()) {
+                    generator.writeArrayFieldStart("bound_address");
+                    generator.writeString("[fe80::1]:" + port);
+                    generator.writeString("[::1]:" + port);
+                    generator.writeString("127.0.0.1:" + port);
+                    generator.writeEndArray();
+                }
+                if (random().nextBoolean()) {
+                    generator.writeObjectFieldStart("bogus_object");
+                    generator.writeEndObject();
+                }
+                generator.writeStringField("publish_address", httpHost.toHostString());
+                if (random().nextBoolean()) {
+                    generator.writeNumberField("max_content_length_in_bytes", 104857600);
+                }
+                generator.writeEndObject();
+            }
+            if (random().nextBoolean()) {
+                String[] roles = {"master", "data", "ingest"};
+                int numRoles = RandomInts.randomIntBetween(random(), 0, 3);
+                Set<String> nodeRoles = new HashSet<>(numRoles);
+                for (int j = 0; j < numRoles; j++) {
+                    String role;
+                    do {
+                        role = RandomPicks.randomFrom(random(), roles);
+                    } while(nodeRoles.add(role) == false);
+                }
+                generator.writeArrayFieldStart("roles");
+                for (String nodeRole : nodeRoles) {
+                    generator.writeString(nodeRole);
+                }
+                generator.writeEndArray();
+            }
+            int numAttributes = RandomInts.randomIntBetween(random(), 0, 3);
+            Map<String, String> attributes = new HashMap<>(numAttributes);
+            for (int j = 0; j < numAttributes; j++) {
+                attributes.put("attr" + j, "value" + j);
+            }
+            if (numAttributes > 0) {
+                generator.writeObjectFieldStart("attributes");
+            }
+            for (Map.Entry<String, String> entry : attributes.entrySet()) {
+                generator.writeStringField(entry.getKey(), entry.getValue());
+            }
+            if (numAttributes > 0) {
+                generator.writeEndObject();
+            }
+            generator.writeEndObject();
+        }
+        generator.writeEndObject();
+        generator.writeEndObject();
+        generator.close();
+        return SniffResponse.buildResponse(writer.toString(), hosts);
+    }
+
+    private static class SniffResponse {
+        private final String nodesInfoBody;
+        private final int nodesInfoResponseCode;
+        private final List<HttpHost> hosts;
+        private final boolean isFailure;
+
+        SniffResponse(String nodesInfoBody, List<HttpHost> hosts, boolean isFailure) {
+            this.nodesInfoBody = nodesInfoBody;
+            this.hosts = hosts;
+            this.isFailure = isFailure;
+            if (isFailure) {
+                this.nodesInfoResponseCode = randomErrorResponseCode();
+            } else {
+                this.nodesInfoResponseCode = 200;
+            }
+        }
+
+        static SniffResponse buildFailure() {
+            return new SniffResponse("", Collections.<HttpHost>emptyList(), true);
+        }
+
+        static SniffResponse buildResponse(String nodesInfoBody, List<HttpHost> hosts) {
+            return new SniffResponse(nodesInfoBody, hosts, false);
+        }
+    }
+
+    private static int randomErrorResponseCode() {
+        return RandomInts.randomIntBetween(random(), 400, 599);
+    }
+}

+ 13 - 15
test/framework/src/main/java/org/elasticsearch/test/rest/client/RestException.java → client-sniffer/src/test/java/org/elasticsearch/client/sniff/MockHostsSniffer.java

@@ -16,26 +16,24 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.elasticsearch.test.rest.client;
 
-/**
- * Thrown when a status code that holds an error is received (unless needs to be ignored)
- * Holds the original {@link RestResponse}
- */
-public class RestException extends Exception {
+package org.elasticsearch.client.sniff;
 
-    private final RestResponse restResponse;
+import org.apache.http.HttpHost;
 
-    public RestException(String message, RestResponse restResponse) {
-        super(message);
-        this.restResponse = restResponse;
-    }
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
 
-    public RestResponse restResponse() {
-        return restResponse;
+class MockHostsSniffer extends HostsSniffer {
+    MockHostsSniffer() {
+        super(null, -1, null);
     }
 
-    public int statusCode() {
-        return restResponse.getStatusCode();
+    @Override
+    public List<HttpHost> sniffHosts() throws IOException {
+        List<HttpHost> hosts = new ArrayList<>();
+        hosts.add(new HttpHost("localhost", 9200));
+        return hosts;
     }
 }

+ 57 - 0
client-sniffer/src/test/java/org/elasticsearch/client/sniff/SniffOnFailureListenerTests.java

@@ -0,0 +1,57 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client.sniff;
+
+import org.apache.http.HttpHost;
+import org.apache.lucene.util.LuceneTestCase;
+import org.elasticsearch.client.RestClient;
+
+public class SniffOnFailureListenerTests extends LuceneTestCase {
+
+    public void testSetSniffer() throws Exception {
+        SniffOnFailureListener listener = new SniffOnFailureListener();
+
+        try {
+            listener.onFailure(null);
+            fail("should have failed");
+        } catch(IllegalStateException e) {
+            assertEquals("sniffer was not set, unable to sniff on failure", e.getMessage());
+        }
+
+        try {
+            listener.setSniffer(null);
+            fail("should have failed");
+        } catch(NullPointerException e) {
+            assertEquals("sniffer must not be null", e.getMessage());
+        }
+
+        RestClient restClient = RestClient.builder(new HttpHost("localhost", 9200)).build();
+        try (Sniffer sniffer = Sniffer.builder(restClient, new MockHostsSniffer()).build()) {
+            listener.setSniffer(sniffer);
+            try {
+                listener.setSniffer(sniffer);
+                fail("should have failed");
+            } catch(IllegalStateException e) {
+                assertEquals("sniffer can only be set once", e.getMessage());
+            }
+            listener.onFailure(new HttpHost("localhost", 9200));
+        }
+    }
+}

+ 84 - 0
client-sniffer/src/test/java/org/elasticsearch/client/sniff/SnifferBuilderTests.java

@@ -0,0 +1,84 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client.sniff;
+
+import com.carrotsearch.randomizedtesting.generators.RandomInts;
+import org.apache.http.HttpHost;
+import org.apache.lucene.util.LuceneTestCase;
+import org.elasticsearch.client.RestClient;
+
+public class SnifferBuilderTests extends LuceneTestCase {
+
+    public void testBuild() throws Exception {
+        int numNodes = RandomInts.randomIntBetween(random(), 1, 5);
+        HttpHost[] hosts = new HttpHost[numNodes];
+        for (int i = 0; i < numNodes; i++) {
+            hosts[i] = new HttpHost("localhost", 9200 + i);
+        }
+
+        HostsSniffer hostsSniffer = new MockHostsSniffer();
+
+        try (RestClient client = RestClient.builder(hosts).build()) {
+            try {
+                Sniffer.builder(null, hostsSniffer).build();
+                fail("should have failed");
+            } catch(NullPointerException e) {
+                assertEquals("restClient cannot be null", e.getMessage());
+            }
+
+            try {
+                Sniffer.builder(client, null).build();
+                fail("should have failed");
+            } catch(NullPointerException e) {
+                assertEquals("hostsSniffer cannot be null", e.getMessage());
+            }
+
+            try {
+                Sniffer.builder(client, hostsSniffer).setSniffIntervalMillis(RandomInts.randomIntBetween(random(), Integer.MIN_VALUE, 0));
+                fail("should have failed");
+            } catch(IllegalArgumentException e) {
+                assertEquals("sniffIntervalMillis must be greater than 0", e.getMessage());
+            }
+
+            try {
+                Sniffer.builder(client, hostsSniffer)
+                        .setSniffAfterFailureDelayMillis(RandomInts.randomIntBetween(random(), Integer.MIN_VALUE, 0));
+                fail("should have failed");
+            } catch(IllegalArgumentException e) {
+                assertEquals("sniffAfterFailureDelayMillis must be greater than 0", e.getMessage());
+            }
+
+            try (Sniffer sniffer = Sniffer.builder(client, hostsSniffer).build()) {
+                assertNotNull(sniffer);
+            }
+
+            Sniffer.Builder builder = Sniffer.builder(client, hostsSniffer);
+            if (random().nextBoolean()) {
+                builder.setSniffIntervalMillis(RandomInts.randomIntBetween(random(), 1, Integer.MAX_VALUE));
+            }
+            if (random().nextBoolean()) {
+                builder.setSniffAfterFailureDelayMillis(RandomInts.randomIntBetween(random(), 1, Integer.MAX_VALUE));
+            }
+            try (Sniffer sniffer = builder.build()) {
+                assertNotNull(sniffer);
+            }
+        }
+    }
+}

+ 80 - 0
client/build.gradle

@@ -0,0 +1,80 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.elasticsearch.gradle.precommit.PrecommitTasks
+import org.gradle.api.JavaVersion
+
+apply plugin: 'elasticsearch.build'
+apply plugin: 'ru.vyarus.animalsniffer'
+
+targetCompatibility = JavaVersion.VERSION_1_7
+sourceCompatibility = JavaVersion.VERSION_1_7
+
+dependencies {
+  compile "org.apache.httpcomponents:httpclient:${versions.httpclient}"
+  compile "org.apache.httpcomponents:httpcore:${versions.httpcore}"
+  compile "commons-codec:commons-codec:${versions.commonscodec}"
+  compile "commons-logging:commons-logging:${versions.commonslogging}"
+
+  testCompile "com.carrotsearch.randomizedtesting:randomizedtesting-runner:${versions.randomizedrunner}"
+  testCompile "junit:junit:${versions.junit}"
+  testCompile "org.hamcrest:hamcrest-all:${versions.hamcrest}"
+  //we use the last lucene-test version that is compatible with java 1.7
+  testCompile "org.apache.lucene:lucene-test-framework:5.5.1"
+  testCompile "org.apache.lucene:lucene-core:5.5.1"
+  testCompile "org.apache.lucene:lucene-codecs:5.5.1"
+  testCompile "org.elasticsearch:securemock:${versions.securemock}"
+  testCompile "org.codehaus.mojo:animal-sniffer-annotations:1.15"
+  signature "org.codehaus.mojo.signature:java17:1.0@signature"
+}
+
+forbiddenApisMain {
+  //client does not depend on core, so only jdk signatures should be checked
+  signaturesURLs = [PrecommitTasks.getResource('/forbidden/jdk-signatures.txt')]
+}
+
+forbiddenApisTest {
+  //we are using jdk-internal instead of jdk-non-portable to allow for com.sun.net.httpserver.* usage
+  bundledSignatures -= 'jdk-non-portable'
+  bundledSignatures += 'jdk-internal'
+  //client does not depend on core, so only jdk signatures should be checked
+  signaturesURLs = [PrecommitTasks.getResource('/forbidden/jdk-signatures.txt')]
+}
+
+//JarHell is part of es core, which we don't want to pull in
+jarHell.enabled=false
+
+namingConventions {
+  //we don't have integration tests
+  skipIntegTestInDisguise = true
+}
+
+thirdPartyAudit.excludes = [
+  //commons-logging optional dependencies
+  'org.apache.avalon.framework.logger.Logger',
+  'org.apache.log.Hierarchy',
+  'org.apache.log.Logger',
+  'org.apache.log4j.Category',
+  'org.apache.log4j.Level',
+  'org.apache.log4j.Logger',
+  'org.apache.log4j.Priority',
+  //commons-logging provided dependencies
+  'javax.servlet.ServletContextEvent',
+  'javax.servlet.ServletContextListener'
+]

+ 1 - 0
client/licenses/commons-codec-1.10.jar.sha1

@@ -0,0 +1 @@
+4b95f4897fa13f2cd904aee711aeafc0c5295cd8

+ 202 - 0
client/licenses/commons-codec-LICENSE.txt

@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 17 - 0
client/licenses/commons-codec-NOTICE.txt

@@ -0,0 +1,17 @@
+Apache Commons Codec
+Copyright 2002-2014 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
+
+src/test/org/apache/commons/codec/language/DoubleMetaphoneTest.java
+contains test data from http://aspell.net/test/orig/batch0.tab.
+Copyright (C) 2002 Kevin Atkinson (kevina@gnu.org)
+
+===============================================================================
+
+The content of package org.apache.commons.codec.language.bm has been translated
+from the original php source code available at http://stevemorse.org/phoneticinfo.htm
+with permission from the original authors.
+Original source copyright:
+Copyright (c) 2008 Alexander Beider & Stephen P. Morse.

+ 1 - 0
client/licenses/commons-logging-1.1.3.jar.sha1

@@ -0,0 +1 @@
+f6f66e966c70a83ffbdb6f17a0919eaf7c8aca7f

+ 202 - 0
client/licenses/commons-logging-LICENSE.txt

@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 6 - 0
client/licenses/commons-logging-NOTICE.txt

@@ -0,0 +1,6 @@
+Apache Commons Logging
+Copyright 2003-2014 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
+

+ 1 - 0
client/licenses/httpclient-4.5.2.jar.sha1

@@ -0,0 +1 @@
+733db77aa8d9b2d68015189df76ab06304406e50

+ 558 - 0
client/licenses/httpclient-LICENSE.txt

@@ -0,0 +1,558 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+=========================================================================
+
+This project includes Public Suffix List copied from
+<https://publicsuffix.org/list/effective_tld_names.dat>
+licensed under the terms of the Mozilla Public License, v. 2.0
+
+Full license text: <http://mozilla.org/MPL/2.0/>
+
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+    means each individual or legal entity that creates, contributes to
+    the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+    means the combination of the Contributions of others (if any) used
+    by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+    means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+    means Source Code Form to which the initial Contributor has attached
+    the notice in Exhibit A, the Executable Form of such Source Code
+    Form, and Modifications of such Source Code Form, in each case
+    including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+    means
+
+    (a) that the initial Contributor has attached the notice described
+        in Exhibit B to the Covered Software; or
+
+    (b) that the Covered Software was made available under the terms of
+        version 1.1 or earlier of the License, but not also under the
+        terms of a Secondary License.
+
+1.6. "Executable Form"
+    means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+    means a work that combines Covered Software with other material, in
+    a separate file or files, that is not Covered Software.
+
+1.8. "License"
+    means this document.
+
+1.9. "Licensable"
+    means having the right to grant, to the maximum extent possible,
+    whether at the time of the initial grant or subsequently, any and
+    all of the rights conveyed by this License.
+
+1.10. "Modifications"
+    means any of the following:
+
+    (a) any file in Source Code Form that results from an addition to,
+        deletion from, or modification of the contents of Covered
+        Software; or
+
+    (b) any new file in Source Code Form that contains any Covered
+        Software.
+
+1.11. "Patent Claims" of a Contributor
+    means any patent claim(s), including without limitation, method,
+    process, and apparatus claims, in any patent Licensable by such
+    Contributor that would be infringed, but for the grant of the
+    License, by the making, using, selling, offering for sale, having
+    made, import, or transfer of either its Contributions or its
+    Contributor Version.
+
+1.12. "Secondary License"
+    means either the GNU General Public License, Version 2.0, the GNU
+    Lesser General Public License, Version 2.1, the GNU Affero General
+    Public License, Version 3.0, or any later versions of those
+    licenses.
+
+1.13. "Source Code Form"
+    means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+    means an individual or a legal entity exercising rights under this
+    License. For legal entities, "You" includes any entity that
+    controls, is controlled by, or is under common control with You. For
+    purposes of this definition, "control" means (a) the power, direct
+    or indirect, to cause the direction or management of such entity,
+    whether by contract or otherwise, or (b) ownership of more than
+    fifty percent (50%) of the outstanding shares or beneficial
+    ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+    Licensable by such Contributor to use, reproduce, make available,
+    modify, display, perform, distribute, and otherwise exploit its
+    Contributions, either on an unmodified basis, with Modifications, or
+    as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+    for sale, have made, import, and otherwise transfer either its
+    Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+    or
+
+(b) for infringements caused by: (i) Your and any other third party's
+    modifications of Covered Software, or (ii) the combination of its
+    Contributions with other software (except as part of its Contributor
+    Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+    its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+    Form, as described in Section 3.1, and You must inform recipients of
+    the Executable Form how they can obtain a copy of such Source Code
+    Form by reasonable means in a timely manner, at a charge no more
+    than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+    License, or sublicense it under different terms, provided that the
+    license for the Executable Form does not attempt to limit or alter
+    the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+*                                                                      *
+*  6. Disclaimer of Warranty                                           *
+*  -------------------------                                           *
+*                                                                      *
+*  Covered Software is provided under this License on an "as is"       *
+*  basis, without warranty of any kind, either expressed, implied, or  *
+*  statutory, including, without limitation, warranties that the       *
+*  Covered Software is free of defects, merchantable, fit for a        *
+*  particular purpose or non-infringing. The entire risk as to the     *
+*  quality and performance of the Covered Software is with You.        *
+*  Should any Covered Software prove defective in any respect, You     *
+*  (not any Contributor) assume the cost of any necessary servicing,   *
+*  repair, or correction. This disclaimer of warranty constitutes an   *
+*  essential part of this License. No use of any Covered Software is   *
+*  authorized under this License except under this disclaimer.         *
+*                                                                      *
+************************************************************************
+
+************************************************************************
+*                                                                      *
+*  7. Limitation of Liability                                          *
+*  --------------------------                                          *
+*                                                                      *
+*  Under no circumstances and under no legal theory, whether tort      *
+*  (including negligence), contract, or otherwise, shall any           *
+*  Contributor, or anyone who distributes Covered Software as          *
+*  permitted above, be liable to You for any direct, indirect,         *
+*  special, incidental, or consequential damages of any character      *
+*  including, without limitation, damages for lost profits, loss of    *
+*  goodwill, work stoppage, computer failure or malfunction, or any    *
+*  and all other commercial damages or losses, even if such party      *
+*  shall have been informed of the possibility of such damages. This   *
+*  limitation of liability shall not apply to liability for death or   *
+*  personal injury resulting from such party's negligence to the       *
+*  extent applicable law prohibits such limitation. Some               *
+*  jurisdictions do not allow the exclusion or limitation of           *
+*  incidental or consequential damages, so this exclusion and          *
+*  limitation may not apply to You.                                    *
+*                                                                      *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+  This Source Code Form is subject to the terms of the Mozilla Public
+  License, v. 2.0. If a copy of the MPL was not distributed with this
+  file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+  This Source Code Form is "Incompatible With Secondary Licenses", as
+  defined by the Mozilla Public License, v. 2.0.

+ 6 - 0
client/licenses/httpclient-NOTICE.txt

@@ -0,0 +1,6 @@
+Apache HttpComponents Client
+Copyright 1999-2016 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
+

+ 1 - 0
client/licenses/httpcore-4.4.4.jar.sha1

@@ -0,0 +1 @@
+b31526a230871fbe285fbcbe2813f9c0839ae9b0

+ 558 - 0
client/licenses/httpcore-LICENSE.txt

@@ -0,0 +1,558 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+=========================================================================
+
+This project includes Public Suffix List copied from
+<https://publicsuffix.org/list/effective_tld_names.dat>
+licensed under the terms of the Mozilla Public License, v. 2.0
+
+Full license text: <http://mozilla.org/MPL/2.0/>
+
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+    means each individual or legal entity that creates, contributes to
+    the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+    means the combination of the Contributions of others (if any) used
+    by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+    means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+    means Source Code Form to which the initial Contributor has attached
+    the notice in Exhibit A, the Executable Form of such Source Code
+    Form, and Modifications of such Source Code Form, in each case
+    including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+    means
+
+    (a) that the initial Contributor has attached the notice described
+        in Exhibit B to the Covered Software; or
+
+    (b) that the Covered Software was made available under the terms of
+        version 1.1 or earlier of the License, but not also under the
+        terms of a Secondary License.
+
+1.6. "Executable Form"
+    means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+    means a work that combines Covered Software with other material, in
+    a separate file or files, that is not Covered Software.
+
+1.8. "License"
+    means this document.
+
+1.9. "Licensable"
+    means having the right to grant, to the maximum extent possible,
+    whether at the time of the initial grant or subsequently, any and
+    all of the rights conveyed by this License.
+
+1.10. "Modifications"
+    means any of the following:
+
+    (a) any file in Source Code Form that results from an addition to,
+        deletion from, or modification of the contents of Covered
+        Software; or
+
+    (b) any new file in Source Code Form that contains any Covered
+        Software.
+
+1.11. "Patent Claims" of a Contributor
+    means any patent claim(s), including without limitation, method,
+    process, and apparatus claims, in any patent Licensable by such
+    Contributor that would be infringed, but for the grant of the
+    License, by the making, using, selling, offering for sale, having
+    made, import, or transfer of either its Contributions or its
+    Contributor Version.
+
+1.12. "Secondary License"
+    means either the GNU General Public License, Version 2.0, the GNU
+    Lesser General Public License, Version 2.1, the GNU Affero General
+    Public License, Version 3.0, or any later versions of those
+    licenses.
+
+1.13. "Source Code Form"
+    means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+    means an individual or a legal entity exercising rights under this
+    License. For legal entities, "You" includes any entity that
+    controls, is controlled by, or is under common control with You. For
+    purposes of this definition, "control" means (a) the power, direct
+    or indirect, to cause the direction or management of such entity,
+    whether by contract or otherwise, or (b) ownership of more than
+    fifty percent (50%) of the outstanding shares or beneficial
+    ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+    Licensable by such Contributor to use, reproduce, make available,
+    modify, display, perform, distribute, and otherwise exploit its
+    Contributions, either on an unmodified basis, with Modifications, or
+    as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+    for sale, have made, import, and otherwise transfer either its
+    Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+    or
+
+(b) for infringements caused by: (i) Your and any other third party's
+    modifications of Covered Software, or (ii) the combination of its
+    Contributions with other software (except as part of its Contributor
+    Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+    its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+    Form, as described in Section 3.1, and You must inform recipients of
+    the Executable Form how they can obtain a copy of such Source Code
+    Form by reasonable means in a timely manner, at a charge no more
+    than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+    License, or sublicense it under different terms, provided that the
+    license for the Executable Form does not attempt to limit or alter
+    the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+*                                                                      *
+*  6. Disclaimer of Warranty                                           *
+*  -------------------------                                           *
+*                                                                      *
+*  Covered Software is provided under this License on an "as is"       *
+*  basis, without warranty of any kind, either expressed, implied, or  *
+*  statutory, including, without limitation, warranties that the       *
+*  Covered Software is free of defects, merchantable, fit for a        *
+*  particular purpose or non-infringing. The entire risk as to the     *
+*  quality and performance of the Covered Software is with You.        *
+*  Should any Covered Software prove defective in any respect, You     *
+*  (not any Contributor) assume the cost of any necessary servicing,   *
+*  repair, or correction. This disclaimer of warranty constitutes an   *
+*  essential part of this License. No use of any Covered Software is   *
+*  authorized under this License except under this disclaimer.         *
+*                                                                      *
+************************************************************************
+
+************************************************************************
+*                                                                      *
+*  7. Limitation of Liability                                          *
+*  --------------------------                                          *
+*                                                                      *
+*  Under no circumstances and under no legal theory, whether tort      *
+*  (including negligence), contract, or otherwise, shall any           *
+*  Contributor, or anyone who distributes Covered Software as          *
+*  permitted above, be liable to You for any direct, indirect,         *
+*  special, incidental, or consequential damages of any character      *
+*  including, without limitation, damages for lost profits, loss of    *
+*  goodwill, work stoppage, computer failure or malfunction, or any    *
+*  and all other commercial damages or losses, even if such party      *
+*  shall have been informed of the possibility of such damages. This   *
+*  limitation of liability shall not apply to liability for death or   *
+*  personal injury resulting from such party's negligence to the       *
+*  extent applicable law prohibits such limitation. Some               *
+*  jurisdictions do not allow the exclusion or limitation of           *
+*  incidental or consequential damages, so this exclusion and          *
+*  limitation may not apply to You.                                    *
+*                                                                      *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+  This Source Code Form is subject to the terms of the Mozilla Public
+  License, v. 2.0. If a copy of the MPL was not distributed with this
+  file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+  This Source Code Form is "Incompatible With Secondary Licenses", as
+  defined by the Mozilla Public License, v. 2.0.

+ 6 - 0
client/licenses/httpcore-NOTICE.txt

@@ -0,0 +1,6 @@
+Apache HttpComponents Client
+Copyright 1999-2016 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
+

+ 71 - 0
client/src/main/java/org/elasticsearch/client/DeadHostState.java

@@ -0,0 +1,71 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Holds the state of a dead connection to a host. Keeps track of how many failed attempts were performed and
+ * when the host should be retried (based on number of previous failed attempts).
+ * Class is immutable, a new copy of it should be created each time the state has to be changed.
+ */
+final class DeadHostState {
+
+    private static final long MIN_CONNECTION_TIMEOUT_NANOS = TimeUnit.MINUTES.toNanos(1);
+    private static final long MAX_CONNECTION_TIMEOUT_NANOS = TimeUnit.MINUTES.toNanos(30);
+
+    static final DeadHostState INITIAL_DEAD_STATE = new DeadHostState();
+
+    private final int failedAttempts;
+    private final long deadUntilNanos;
+
+    private DeadHostState() {
+        this.failedAttempts = 1;
+        this.deadUntilNanos = System.nanoTime() + MIN_CONNECTION_TIMEOUT_NANOS;
+    }
+
+    /**
+     * We keep track of how many times a certain node fails consecutively. The higher that number is the longer we will wait
+     * to retry that same node again. Minimum is 1 minute (for a node the only failed once), maximum is 30 minutes (for a node
+     * that failed many consecutive times).
+     */
+    DeadHostState(DeadHostState previousDeadHostState) {
+        long timeoutNanos = (long)Math.min(MIN_CONNECTION_TIMEOUT_NANOS * 2 * Math.pow(2, previousDeadHostState.failedAttempts * 0.5 - 1),
+                MAX_CONNECTION_TIMEOUT_NANOS);
+        this.deadUntilNanos = System.nanoTime() + timeoutNanos;
+        this.failedAttempts = previousDeadHostState.failedAttempts + 1;
+    }
+
+    /**
+     * Returns the timestamp (nanos) till the host is supposed to stay dead without being retried.
+     * After that the host should be retried.
+     */
+    long getDeadUntilNanos() {
+        return deadUntilNanos;
+    }
+
+    @Override
+    public String toString() {
+        return "DeadHostState{" +
+                "failedAttempts=" + failedAttempts +
+                ", deadUntilNanos=" + deadUntilNanos +
+                '}';
+    }
+}

+ 5 - 4
test/framework/src/main/java/org/elasticsearch/test/rest/client/http/HttpDeleteWithEntity.java → client/src/main/java/org/elasticsearch/client/HttpDeleteWithEntity.java

@@ -16,8 +16,9 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.elasticsearch.test.rest.client.http;
+package org.elasticsearch.client;
 
+import org.apache.http.client.methods.HttpDelete;
 import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
 
 import java.net.URI;
@@ -25,11 +26,11 @@ import java.net.URI;
 /**
  * Allows to send DELETE requests providing a body (not supported out of the box)
  */
-public class HttpDeleteWithEntity extends HttpEntityEnclosingRequestBase {
+final class HttpDeleteWithEntity extends HttpEntityEnclosingRequestBase {
 
-    public final static String METHOD_NAME = "DELETE";
+    final static String METHOD_NAME = HttpDelete.METHOD_NAME;
 
-    public HttpDeleteWithEntity(final URI uri) {
+    HttpDeleteWithEntity(final URI uri) {
         setURI(uri);
     }
 

+ 5 - 4
test/framework/src/main/java/org/elasticsearch/test/rest/client/http/HttpGetWithEntity.java → client/src/main/java/org/elasticsearch/client/HttpGetWithEntity.java

@@ -16,20 +16,21 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.elasticsearch.test.rest.client.http;
+package org.elasticsearch.client;
 
 import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
+import org.apache.http.client.methods.HttpGet;
 
 import java.net.URI;
 
 /**
  * Allows to send GET requests providing a body (not supported out of the box)
  */
-public class HttpGetWithEntity extends HttpEntityEnclosingRequestBase {
+final class HttpGetWithEntity extends HttpEntityEnclosingRequestBase {
 
-    public final static String METHOD_NAME = "GET";
+    final static String METHOD_NAME = HttpGet.METHOD_NAME;
 
-    public HttpGetWithEntity(final URI uri) {
+    HttpGetWithEntity(final URI uri) {
         setURI(uri);
     }
 

+ 146 - 0
client/src/main/java/org/elasticsearch/client/RequestLogger.java

@@ -0,0 +1,146 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpEntityEnclosingRequest;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.entity.BufferedHttpEntity;
+import org.apache.http.entity.ContentType;
+import org.apache.http.util.EntityUtils;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Helper class that exposes static methods to unify the way requests are logged.
+ * Includes trace logging to log complete requests and responses in curl format.
+ * Useful for debugging, manually sending logged requests via curl and checking their responses.
+ * Trace logging is a feature that all the language clients provide.
+ */
+final class RequestLogger {
+
+    private static final Log tracer = LogFactory.getLog("tracer");
+
+    private RequestLogger() {
+    }
+
+    /**
+     * Logs a request that yielded a response
+     */
+    static void logResponse(Log logger, HttpUriRequest request, HttpHost host, HttpResponse httpResponse) {
+        if (logger.isDebugEnabled()) {
+            logger.debug("request [" + request.getMethod() + " " + host + request.getRequestLine().getUri() +
+                    "] returned [" + httpResponse.getStatusLine() + "]");
+        }
+        if (tracer.isTraceEnabled()) {
+            String requestLine;
+            try {
+                requestLine = buildTraceRequest(request, host);
+            } catch(IOException e) {
+                requestLine = "";
+                tracer.trace("error while reading request for trace purposes", e);
+            }
+            String responseLine;
+            try {
+                responseLine = buildTraceResponse(httpResponse);
+            } catch(IOException e) {
+                responseLine = "";
+                tracer.trace("error while reading response for trace purposes", e);
+            }
+            tracer.trace(requestLine + '\n' + responseLine);
+        }
+    }
+
+    /**
+     * Logs a request that failed
+     */
+    static void logFailedRequest(Log logger, HttpUriRequest request, HttpHost host, IOException e) {
+        logger.debug("request [" + request.getMethod() + " " + host + request.getRequestLine().getUri() + "] failed", e);
+        if (logger.isTraceEnabled()) {
+            String traceRequest;
+            try {
+                traceRequest = buildTraceRequest(request, host);
+            } catch (IOException e1) {
+                tracer.trace("error while reading request for trace purposes", e);
+                traceRequest = "";
+            }
+            tracer.trace(traceRequest);
+        }
+    }
+
+    /**
+     * Creates curl output for given request
+     */
+    static String buildTraceRequest(HttpUriRequest request, HttpHost host) throws IOException {
+        String requestLine = "curl -iX " + request.getMethod() + " '" + host + request.getRequestLine().getUri() + "'";
+        if (request instanceof  HttpEntityEnclosingRequest) {
+            HttpEntityEnclosingRequest enclosingRequest = (HttpEntityEnclosingRequest) request;
+            if (enclosingRequest.getEntity() != null) {
+                requestLine += " -d '";
+                HttpEntity entity = enclosingRequest.getEntity();
+                if (entity.isRepeatable() == false) {
+                    entity = new BufferedHttpEntity(enclosingRequest.getEntity());
+                    enclosingRequest.setEntity(entity);
+                }
+                requestLine += EntityUtils.toString(entity, StandardCharsets.UTF_8) + "'";
+            }
+        }
+        return requestLine;
+    }
+
+    /**
+     * Creates curl output for given response
+     */
+    static String buildTraceResponse(HttpResponse httpResponse) throws IOException {
+        String responseLine = "# " + httpResponse.getStatusLine().toString();
+        for (Header header : httpResponse.getAllHeaders()) {
+            responseLine += "\n# " + header.getName() + ": " + header.getValue();
+        }
+        responseLine += "\n#";
+        HttpEntity entity = httpResponse.getEntity();
+        if (entity != null) {
+            if (entity.isRepeatable() == false) {
+                entity = new BufferedHttpEntity(entity);
+            }
+            httpResponse.setEntity(entity);
+            ContentType contentType = ContentType.get(entity);
+            Charset charset = StandardCharsets.UTF_8;
+            if (contentType != null) {
+                charset = contentType.getCharset();
+            }
+            try (BufferedReader reader = new BufferedReader(new InputStreamReader(entity.getContent(), charset))) {
+                String line;
+                while( (line = reader.readLine()) != null) {
+                    responseLine += "\n# " + line;
+                }
+            }
+        }
+        return responseLine;
+    }
+}

+ 115 - 0
client/src/main/java/org/elasticsearch/client/Response.java

@@ -0,0 +1,115 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpHost;
+import org.apache.http.RequestLine;
+import org.apache.http.StatusLine;
+import org.apache.http.client.methods.CloseableHttpResponse;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.Objects;
+
+/**
+ * Holds an elasticsearch response. It wraps the {@link CloseableHttpResponse} response and associates it with
+ * its corresponding {@link RequestLine} and {@link HttpHost}.
+ * It must be closed to free any resource held by it, as well as the corresponding connection in the connection pool.
+ */
+public class Response implements Closeable {
+
+    private final RequestLine requestLine;
+    private final HttpHost host;
+    private final CloseableHttpResponse response;
+
+    Response(RequestLine requestLine, HttpHost host, CloseableHttpResponse response) {
+        Objects.requireNonNull(requestLine, "requestLine cannot be null");
+        Objects.requireNonNull(host, "node cannot be null");
+        Objects.requireNonNull(response, "response cannot be null");
+        this.requestLine = requestLine;
+        this.host = host;
+        this.response = response;
+    }
+
+    /**
+     * Returns the request line that generated this response
+     */
+    public RequestLine getRequestLine() {
+        return requestLine;
+    }
+
+    /**
+     * Returns the node that returned this response
+     */
+    public HttpHost getHost() {
+        return host;
+    }
+
+    /**
+     * Returns the status line of the current response
+     */
+    public StatusLine getStatusLine() {
+        return response.getStatusLine();
+    }
+
+    /**
+     * Returns all the response headers
+     */
+    public Header[] getHeaders() {
+        return response.getAllHeaders();
+    }
+
+    /**
+     * Returns the value of the first header with a specified name of this message.
+     * If there is more than one matching header in the message the first element is returned.
+     * If there is no matching header in the message <code>null</code> is returned.
+     */
+    public String getHeader(String name) {
+        Header header = response.getFirstHeader(name);
+        if (header == null) {
+            return null;
+        }
+        return header.getValue();
+    }
+
+    /**
+     * Returns the response body available, null otherwise
+     * @see HttpEntity
+     */
+    public HttpEntity getEntity() {
+        return response.getEntity();
+    }
+
+    @Override
+    public String toString() {
+        return "Response{" +
+                "requestLine=" + requestLine +
+                ", host=" + host +
+                ", response=" + response.getStatusLine() +
+                '}';
+    }
+
+    @Override
+    public void close() throws IOException {
+        this.response.close();
+    }
+}

+ 66 - 0
client/src/main/java/org/elasticsearch/client/ResponseException.java

@@ -0,0 +1,66 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client;
+
+import java.io.IOException;
+
+/**
+ * Exception thrown when an elasticsearch node responds to a request with a status code that indicates an error.
+ * Note that the response body gets passed in as a string and read eagerly, which means that the Response object
+ * is expected to be closed and available only to read metadata like status line, request line, response headers.
+ */
+public class ResponseException extends IOException {
+
+    private Response response;
+    private final String responseBody;
+
+    ResponseException(Response response, String responseBody) throws IOException {
+        super(buildMessage(response,responseBody));
+        this.response = response;
+        this.responseBody = responseBody;
+    }
+
+    private static String buildMessage(Response response, String responseBody) {
+        String message = response.getRequestLine().getMethod() + " " + response.getHost() + response.getRequestLine().getUri()
+                + ": " + response.getStatusLine().toString();
+        if (responseBody != null) {
+            message += "\n" + responseBody;
+        }
+        return message;
+    }
+
+    /**
+     * Returns the {@link Response} that caused this exception to be thrown.
+     * Expected to be used only to read metadata like status line, request line, response headers. The response body should
+     * be retrieved using {@link #getResponseBody()}
+     */
+    public Response getResponse() {
+        return response;
+    }
+
+    /**
+     * Returns the response body as a string or null if there wasn't any.
+     * The body is eagerly consumed when an ResponseException gets created, and its corresponding Response
+     * gets closed straightaway so this method is the only way to get back the response body that was returned.
+     */
+    public String getResponseBody() {
+        return responseBody;
+    }
+}

+ 475 - 0
client/src/main/java/org/elasticsearch/client/RestClient.java

@@ -0,0 +1,475 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.elasticsearch.client;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.http.Consts;
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpRequest;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
+import org.apache.http.client.methods.HttpHead;
+import org.apache.http.client.methods.HttpOptions;
+import org.apache.http.client.methods.HttpPatch;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.client.methods.HttpTrace;
+import org.apache.http.client.utils.URIBuilder;
+import org.apache.http.config.Registry;
+import org.apache.http.conn.socket.ConnectionSocketFactory;
+import org.apache.http.entity.ContentType;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.apache.http.util.EntityUtils;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Client that connects to an elasticsearch cluster through http.
+ * Must be created using {@link Builder}, which allows to set all the different options or just rely on defaults.
+ * The hosts that are part of the cluster need to be provided at creation time, but can also be replaced later
+ * by calling {@link #setHosts(HttpHost...)}.
+ * The method {@link #performRequest(String, String, Map, HttpEntity, Header...)} allows to send a request to the cluster. When
+ * sending a request, a host gets selected out of the provided ones in a round-robin fashion. Failing hosts are marked dead and
+ * retried after a certain amount of time (minimum 1 minute, maximum 30 minutes), depending on how many times they previously
+ * failed (the more failures, the later they will be retried). In case of failures all of the alive nodes (or dead nodes that
+ * deserve a retry) are retried till one responds or none of them does, in which case an {@link IOException} will be thrown.
+ *
+ * Requests can be traced by enabling trace logging for "tracer". The trace logger outputs requests and responses in curl format.
+ */
+public final class RestClient implements Closeable {
+
+    private static final Log logger = LogFactory.getLog(RestClient.class);
+    public static ContentType JSON_CONTENT_TYPE = ContentType.create("application/json", Consts.UTF_8);
+
+    private final CloseableHttpClient client;
+    //we don't rely on default headers supported by HttpClient as those cannot be replaced, plus it would get hairy
+    //when we create the HttpClient instance on our own as there would be two different ways to set the default headers.
+    private final Header[] defaultHeaders;
+    private final long maxRetryTimeoutMillis;
+    private final AtomicInteger lastHostIndex = new AtomicInteger(0);
+    private volatile Set<HttpHost> hosts;
+    private final ConcurrentMap<HttpHost, DeadHostState> blacklist = new ConcurrentHashMap<>();
+    private final FailureListener failureListener;
+
+    private RestClient(CloseableHttpClient client, long maxRetryTimeoutMillis, Header[] defaultHeaders,
+                       HttpHost[] hosts, FailureListener failureListener) {
+        this.client = client;
+        this.maxRetryTimeoutMillis = maxRetryTimeoutMillis;
+        this.defaultHeaders = defaultHeaders;
+        this.failureListener = failureListener;
+        setHosts(hosts);
+    }
+
+    /**
+     * Replaces the hosts that the client communicates with.
+     * @see HttpHost
+     */
+    public synchronized void setHosts(HttpHost... hosts) {
+        if (hosts == null || hosts.length == 0) {
+            throw new IllegalArgumentException("hosts must not be null nor empty");
+        }
+        Set<HttpHost> httpHosts = new HashSet<>();
+        for (HttpHost host : hosts) {
+            Objects.requireNonNull(host, "host cannot be null");
+            httpHosts.add(host);
+        }
+        this.hosts = Collections.unmodifiableSet(httpHosts);
+        this.blacklist.clear();
+    }
+
+    /**
+     * Sends a request to the elasticsearch cluster that the current client points to.
+     * Selects a host out of the provided ones in a round-robin fashion. Failing hosts are marked dead and retried after a certain
+     * amount of time (minimum 1 minute, maximum 30 minutes), depending on how many times they previously failed (the more failures,
+     * the later they will be retried). In case of failures all of the alive nodes (or dead nodes that deserve a retry) are retried
+     * till one responds or none of them does, in which case an {@link IOException} will be thrown.
+     *
+     * @param method the http method
+     * @param endpoint the path of the request (without host and port)
+     * @param params the query_string parameters
+     * @param entity the body of the request, null if not applicable
+     * @param headers the optional request headers
+     * @return the response returned by elasticsearch
+     * @throws IOException in case of a problem or the connection was aborted
+     * @throws ClientProtocolException in case of an http protocol error
+     * @throws ResponseException in case elasticsearch responded with a status code that indicated an error
+     */
+    public Response performRequest(String method, String endpoint, Map<String, String> params,
+                                   HttpEntity entity, Header... headers) throws IOException {
+        URI uri = buildUri(endpoint, params);
+        HttpRequestBase request = createHttpRequest(method, uri, entity);
+        setHeaders(request, headers);
+        //we apply a soft margin so that e.g. if a request took 59 seconds and timeout is set to 60 we don't do another attempt
+        long retryTimeoutMillis = Math.round(this.maxRetryTimeoutMillis / (float)100 * 98);
+        IOException lastSeenException = null;
+        long startTime = System.nanoTime();
+        for (HttpHost host : nextHost()) {
+            if (lastSeenException != null) {
+                //in case we are retrying, check whether maxRetryTimeout has been reached, in which case an exception will be thrown
+                long timeElapsedMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
+                long timeout = retryTimeoutMillis - timeElapsedMillis;
+                if (timeout <= 0) {
+                    IOException retryTimeoutException = new IOException(
+                            "request retries exceeded max retry timeout [" + retryTimeoutMillis + "]");
+                    retryTimeoutException.addSuppressed(lastSeenException);
+                    throw retryTimeoutException;
+                }
+                //also reset the request to make it reusable for the next attempt
+                request.reset();
+            }
+
+            CloseableHttpResponse httpResponse;
+            try {
+                httpResponse = client.execute(host, request);
+            } catch(IOException e) {
+                RequestLogger.logFailedRequest(logger, request, host, e);
+                onFailure(host);
+                lastSeenException = addSuppressedException(lastSeenException, e);
+                continue;
+            }
+            Response response = new Response(request.getRequestLine(), host, httpResponse);
+            int statusCode = response.getStatusLine().getStatusCode();
+            if (statusCode < 300 || (request.getMethod().equals(HttpHead.METHOD_NAME) && statusCode == 404) ) {
+                RequestLogger.logResponse(logger, request, host, httpResponse);
+                onResponse(host);
+                return response;
+            }
+            RequestLogger.logResponse(logger, request, host, httpResponse);
+            String responseBody;
+            try {
+                if (response.getEntity() == null) {
+                    responseBody = null;
+                } else {
+                    responseBody = EntityUtils.toString(response.getEntity());
+                }
+            } finally {
+                response.close();
+            }
+            lastSeenException = addSuppressedException(lastSeenException, new ResponseException(response, responseBody));
+            switch(statusCode) {
+                case 502:
+                case 503:
+                case 504:
+                    //mark host dead and retry against next one
+                    onFailure(host);
+                    break;
+                default:
+                    //mark host alive and don't retry, as the error should be a request problem
+                    onResponse(host);
+                    throw lastSeenException;
+            }
+        }
+        //we get here only when we tried all nodes and they all failed
+        assert lastSeenException != null;
+        throw lastSeenException;
+    }
+
+    private void setHeaders(HttpRequest httpRequest, Header[] requestHeaders) {
+        Objects.requireNonNull(requestHeaders, "request headers must not be null");
+        for (Header defaultHeader : defaultHeaders) {
+            httpRequest.setHeader(defaultHeader);
+        }
+        for (Header requestHeader : requestHeaders) {
+            Objects.requireNonNull(requestHeader, "request header must not be null");
+            httpRequest.setHeader(requestHeader);
+        }
+    }
+
+    /**
+     * Returns an iterator of hosts to be used for a request call.
+     * Ideally, the first host is retrieved from the iterator and used successfully for the request.
+     * Otherwise, after each failure the next host should be retrieved from the iterator so that the request can be retried till
+     * the iterator is exhausted. The maximum total of attempts is equal to the number of hosts that are available in the iterator.
+     * The iterator returned will never be empty, rather an {@link IllegalStateException} in case there are no hosts.
+     * In case there are no healthy hosts available, or dead ones to be be retried, one dead host gets returned.
+     */
+    private Iterable<HttpHost> nextHost() {
+        Set<HttpHost> filteredHosts = new HashSet<>(hosts);
+        for (Map.Entry<HttpHost, DeadHostState> entry : blacklist.entrySet()) {
+            if (System.nanoTime() - entry.getValue().getDeadUntilNanos() < 0) {
+                filteredHosts.remove(entry.getKey());
+            }
+        }
+
+        if (filteredHosts.isEmpty()) {
+            //last resort: if there are no good hosts to use, return a single dead one, the one that's closest to being retried
+            List<Map.Entry<HttpHost, DeadHostState>> sortedHosts = new ArrayList<>(blacklist.entrySet());
+            Collections.sort(sortedHosts, new Comparator<Map.Entry<HttpHost, DeadHostState>>() {
+                @Override
+                public int compare(Map.Entry<HttpHost, DeadHostState> o1, Map.Entry<HttpHost, DeadHostState> o2) {
+                    return Long.compare(o1.getValue().getDeadUntilNanos(), o2.getValue().getDeadUntilNanos());
+                }
+            });
+            HttpHost deadHost = sortedHosts.get(0).getKey();
+            logger.trace("resurrecting host [" + deadHost + "]");
+            return Collections.singleton(deadHost);
+        }
+
+        List<HttpHost> rotatedHosts = new ArrayList<>(filteredHosts);
+        Collections.rotate(rotatedHosts, rotatedHosts.size() - lastHostIndex.getAndIncrement());
+        return rotatedHosts;
+    }
+
+    /**
+     * Called after each successful request call.
+     * Receives as an argument the host that was used for the successful request.
+     */
+    private void onResponse(HttpHost host) {
+        DeadHostState removedHost = this.blacklist.remove(host);
+        if (logger.isDebugEnabled() && removedHost != null) {
+            logger.debug("removed host [" + host + "] from blacklist");
+        }
+    }
+
+    /**
+     * Called after each failed attempt.
+     * Receives as an argument the host that was used for the failed attempt.
+     */
+    private void onFailure(HttpHost host) throws IOException {
+        while(true) {
+            DeadHostState previousDeadHostState = blacklist.putIfAbsent(host, DeadHostState.INITIAL_DEAD_STATE);
+            if (previousDeadHostState == null) {
+                logger.debug("added host [" + host + "] to blacklist");
+                break;
+            }
+            if (blacklist.replace(host, previousDeadHostState, new DeadHostState(previousDeadHostState))) {
+                logger.debug("updated host [" + host + "] already in blacklist");
+                break;
+            }
+        }
+        failureListener.onFailure(host);
+    }
+
+    @Override
+    public void close() throws IOException {
+        client.close();
+    }
+
+    private static IOException addSuppressedException(IOException suppressedException, IOException currentException) {
+        if (suppressedException != null) {
+            currentException.addSuppressed(suppressedException);
+        }
+        return currentException;
+    }
+
+    private static HttpRequestBase createHttpRequest(String method, URI uri, HttpEntity entity) {
+        switch(method.toUpperCase(Locale.ROOT)) {
+            case HttpDeleteWithEntity.METHOD_NAME:
+                return addRequestBody(new HttpDeleteWithEntity(uri), entity);
+            case HttpGetWithEntity.METHOD_NAME:
+                return addRequestBody(new HttpGetWithEntity(uri), entity);
+            case HttpHead.METHOD_NAME:
+                return addRequestBody(new HttpHead(uri), entity);
+            case HttpOptions.METHOD_NAME:
+                return addRequestBody(new HttpOptions(uri), entity);
+            case HttpPatch.METHOD_NAME:
+                return addRequestBody(new HttpPatch(uri), entity);
+            case HttpPost.METHOD_NAME:
+                HttpPost httpPost = new HttpPost(uri);
+                addRequestBody(httpPost, entity);
+                return httpPost;
+            case HttpPut.METHOD_NAME:
+                return addRequestBody(new HttpPut(uri), entity);
+            case HttpTrace.METHOD_NAME:
+                return addRequestBody(new HttpTrace(uri), entity);
+            default:
+                throw new UnsupportedOperationException("http method not supported: " + method);
+        }
+    }
+
+    private static HttpRequestBase addRequestBody(HttpRequestBase httpRequest, HttpEntity entity) {
+        if (entity != null) {
+            if (httpRequest instanceof HttpEntityEnclosingRequestBase) {
+                ((HttpEntityEnclosingRequestBase)httpRequest).setEntity(entity);
+            } else {
+                throw new UnsupportedOperationException(httpRequest.getMethod() + " with body is not supported");
+            }
+        }
+        return httpRequest;
+    }
+
+    private static URI buildUri(String path, Map<String, String> params) {
+        Objects.requireNonNull(params, "params must not be null");
+        try {
+            URIBuilder uriBuilder = new URIBuilder(path);
+            for (Map.Entry<String, String> param : params.entrySet()) {
+                uriBuilder.addParameter(param.getKey(), param.getValue());
+            }
+            return uriBuilder.build();
+        } catch(URISyntaxException e) {
+            throw new IllegalArgumentException(e.getMessage(), e);
+        }
+    }
+
+    /**
+     * Returns a new {@link Builder} to help with {@link RestClient} creation.
+     */
+    public static Builder builder(HttpHost... hosts) {
+        return new Builder(hosts);
+    }
+
+    /**
+     * Rest client builder. Helps creating a new {@link RestClient}.
+     */
+    public static final class Builder {
+        public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 1000;
+        public static final int DEFAULT_SOCKET_TIMEOUT_MILLIS = 10000;
+        public static final int DEFAULT_MAX_RETRY_TIMEOUT_MILLIS = DEFAULT_SOCKET_TIMEOUT_MILLIS;
+        public static final int DEFAULT_CONNECTION_REQUEST_TIMEOUT_MILLIS = 500;
+
+        private static final Header[] EMPTY_HEADERS = new Header[0];
+
+        private final HttpHost[] hosts;
+        private CloseableHttpClient httpClient;
+        private int maxRetryTimeout = DEFAULT_MAX_RETRY_TIMEOUT_MILLIS;
+        private Header[] defaultHeaders = EMPTY_HEADERS;
+        private FailureListener failureListener;
+
+        /**
+         * Creates a new builder instance and sets the hosts that the client will send requests to.
+         */
+        private Builder(HttpHost... hosts) {
+            if (hosts == null || hosts.length == 0) {
+                throw new IllegalArgumentException("no hosts provided");
+            }
+            this.hosts = hosts;
+        }
+
+        /**
+         * Sets the http client. A new default one will be created if not
+         * specified, by calling {@link #createDefaultHttpClient(Registry)})}.
+         *
+         * @see CloseableHttpClient
+         */
+        public Builder setHttpClient(CloseableHttpClient httpClient) {
+            this.httpClient = httpClient;
+            return this;
+        }
+
+        /**
+         * Sets the maximum timeout (in milliseconds) to honour in case of multiple retries of the same request.
+         * {@link #DEFAULT_MAX_RETRY_TIMEOUT_MILLIS} if not specified.
+         *
+         * @throws IllegalArgumentException if maxRetryTimeoutMillis is not greater than 0
+         */
+        public Builder setMaxRetryTimeoutMillis(int maxRetryTimeoutMillis) {
+            if (maxRetryTimeoutMillis <= 0) {
+                throw new IllegalArgumentException("maxRetryTimeoutMillis must be greater than 0");
+            }
+            this.maxRetryTimeout = maxRetryTimeoutMillis;
+            return this;
+        }
+
+        /**
+         * Sets the default request headers, to be used when creating the default http client instance.
+         * In case the http client is set through {@link #setHttpClient(CloseableHttpClient)}, the default headers need to be
+         * set to it externally during http client construction.
+         */
+        public Builder setDefaultHeaders(Header[] defaultHeaders) {
+            Objects.requireNonNull(defaultHeaders, "default headers must not be null");
+            for (Header defaultHeader : defaultHeaders) {
+                Objects.requireNonNull(defaultHeader, "default header must not be null");
+            }
+            this.defaultHeaders = defaultHeaders;
+            return this;
+        }
+
+        /**
+         * Sets the {@link FailureListener} to be notified for each request failure
+         */
+        public Builder setFailureListener(FailureListener failureListener) {
+            Objects.requireNonNull(failureListener, "failure listener must not be null");
+            this.failureListener = failureListener;
+            return this;
+        }
+
+        /**
+         * Creates a new {@link RestClient} based on the provided configuration.
+         */
+        public RestClient build() {
+            if (httpClient == null) {
+                httpClient = createDefaultHttpClient(null);
+            }
+            if (failureListener == null) {
+                failureListener = new FailureListener();
+            }
+            return new RestClient(httpClient, maxRetryTimeout, defaultHeaders, hosts, failureListener);
+        }
+
+        /**
+         * Creates a {@link CloseableHttpClient} with default settings. Used when the http client instance is not provided.
+         *
+         * @see CloseableHttpClient
+         */
+        public static CloseableHttpClient createDefaultHttpClient(Registry<ConnectionSocketFactory> socketFactoryRegistry) {
+            PoolingHttpClientConnectionManager connectionManager;
+            if (socketFactoryRegistry == null) {
+                connectionManager = new PoolingHttpClientConnectionManager();
+            } else {
+                connectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
+            }
+            //default settings may be too constraining
+            connectionManager.setDefaultMaxPerRoute(10);
+            connectionManager.setMaxTotal(30);
+
+            //default timeouts are all infinite
+            RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(DEFAULT_CONNECT_TIMEOUT_MILLIS)
+                    .setSocketTimeout(DEFAULT_SOCKET_TIMEOUT_MILLIS)
+                    .setConnectionRequestTimeout(DEFAULT_CONNECTION_REQUEST_TIMEOUT_MILLIS).build();
+            return HttpClientBuilder.create().setConnectionManager(connectionManager).setDefaultRequestConfig(requestConfig).build();
+        }
+    }
+
+    /**
+     * Listener that allows to be notified whenever a failure happens. Useful when sniffing is enabled, so that we can sniff on failure.
+     * The default implementation is a no-op.
+     */
+    public static class FailureListener {
+        /**
+         * Notifies that the host provided as argument has just failed
+         */
+        public void onFailure(HttpHost host) throws IOException {
+
+        }
+    }
+}

+ 42 - 0
client/src/test/java/org/elasticsearch/client/CloseableBasicHttpResponse.java

@@ -0,0 +1,42 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client;
+
+import org.apache.http.StatusLine;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.message.BasicHttpResponse;
+
+import java.io.IOException;
+
+/**
+ * Simple {@link CloseableHttpResponse} impl needed to easily create http responses that are closeable given that
+ * org.apache.http.impl.execchain.HttpResponseProxy is not public.
+ */
+class CloseableBasicHttpResponse extends BasicHttpResponse implements CloseableHttpResponse {
+
+    public CloseableBasicHttpResponse(StatusLine statusline) {
+        super(statusline);
+    }
+
+    @Override
+    public void close() throws IOException {
+        //nothing to close
+    }
+}

+ 145 - 0
client/src/test/java/org/elasticsearch/client/RequestLoggerTests.java

@@ -0,0 +1,145 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client;
+
+import com.carrotsearch.randomizedtesting.generators.RandomInts;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpEntityEnclosingRequest;
+import org.apache.http.HttpHost;
+import org.apache.http.ProtocolVersion;
+import org.apache.http.client.methods.HttpHead;
+import org.apache.http.client.methods.HttpOptions;
+import org.apache.http.client.methods.HttpPatch;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.client.methods.HttpTrace;
+import org.apache.http.entity.InputStreamEntity;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.message.BasicHttpResponse;
+import org.apache.http.message.BasicStatusLine;
+import org.apache.http.util.EntityUtils;
+import org.apache.lucene.util.LuceneTestCase;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+
+public class RequestLoggerTests extends LuceneTestCase {
+
+    public void testTraceRequest() throws IOException, URISyntaxException {
+        HttpHost host = new HttpHost("localhost", 9200, random().nextBoolean() ? "http" : "https");
+        URI uri = new URI("/index/type/_api");
+
+        HttpRequestBase request;
+        int requestType = RandomInts.randomIntBetween(random(), 0, 7);
+        switch(requestType) {
+            case 0:
+                request = new HttpGetWithEntity(uri);
+                break;
+            case 1:
+                request = new HttpPost(uri);
+                break;
+            case 2:
+                request = new HttpPut(uri);
+                break;
+            case 3:
+                request = new HttpDeleteWithEntity(uri);
+                break;
+            case 4:
+                request = new HttpHead(uri);
+                break;
+            case 5:
+                request = new HttpTrace(uri);
+                break;
+            case 6:
+                request = new HttpOptions(uri);
+                break;
+            case 7:
+                request = new HttpPatch(uri);
+                break;
+            default:
+                throw new UnsupportedOperationException();
+        }
+
+        String expected = "curl -iX " + request.getMethod() + " '" + host + uri + "'";
+        boolean hasBody = request instanceof HttpEntityEnclosingRequest && random().nextBoolean();
+        String requestBody = "{ \"field\": \"value\" }";
+        if (hasBody) {
+            expected += " -d '" + requestBody + "'";
+            HttpEntityEnclosingRequest enclosingRequest = (HttpEntityEnclosingRequest) request;
+            HttpEntity entity;
+            if (random().nextBoolean()) {
+                entity = new StringEntity(requestBody, StandardCharsets.UTF_8);
+            } else {
+                entity = new InputStreamEntity(new ByteArrayInputStream(requestBody.getBytes(StandardCharsets.UTF_8)));
+            }
+            enclosingRequest.setEntity(entity);
+        }
+
+        String traceRequest = RequestLogger.buildTraceRequest(request, host);
+        assertThat(traceRequest, equalTo(expected));
+        if (hasBody) {
+            //check that the body is still readable as most entities are not repeatable
+            String body = EntityUtils.toString(((HttpEntityEnclosingRequest) request).getEntity(), StandardCharsets.UTF_8);
+            assertThat(body, equalTo(requestBody));
+        }
+    }
+
+    public void testTraceResponse() throws IOException {
+        ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1);
+        int statusCode = RandomInts.randomIntBetween(random(), 200, 599);
+        String reasonPhrase = "REASON";
+        BasicStatusLine statusLine = new BasicStatusLine(protocolVersion, statusCode, reasonPhrase);
+        String expected = "# " + statusLine.toString();
+        BasicHttpResponse httpResponse = new BasicHttpResponse(statusLine);
+        int numHeaders = RandomInts.randomIntBetween(random(), 0, 3);
+        for (int i = 0; i < numHeaders; i++) {
+            httpResponse.setHeader("header" + i, "value");
+            expected += "\n# header" + i + ": value";
+        }
+        expected += "\n#";
+        boolean hasBody = random().nextBoolean();
+        String responseBody = "{\n  \"field\": \"value\"\n}";
+        if (hasBody) {
+            expected += "\n# {";
+            expected += "\n#   \"field\": \"value\"";
+            expected += "\n# }";
+            HttpEntity entity;
+            if (random().nextBoolean()) {
+                entity = new StringEntity(responseBody, StandardCharsets.UTF_8);
+            } else {
+                entity = new InputStreamEntity(new ByteArrayInputStream(responseBody.getBytes(StandardCharsets.UTF_8)));
+            }
+            httpResponse.setEntity(entity);
+        }
+        String traceResponse = RequestLogger.buildTraceResponse(httpResponse);
+        assertThat(traceResponse, equalTo(expected));
+        if (hasBody) {
+            //check that the body is still readable as most entities are not repeatable
+            String body = EntityUtils.toString(httpResponse.getEntity(), StandardCharsets.UTF_8);
+            assertThat(body, equalTo(responseBody));
+        }
+    }
+}

+ 108 - 0
client/src/test/java/org/elasticsearch/client/RestClientBuilderTests.java

@@ -0,0 +1,108 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client;
+
+import com.carrotsearch.randomizedtesting.generators.RandomInts;
+import org.apache.http.Header;
+import org.apache.http.HttpHost;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.message.BasicHeader;
+import org.apache.lucene.util.LuceneTestCase;
+
+import java.io.IOException;
+
+public class RestClientBuilderTests extends LuceneTestCase {
+
+    public void testBuild() throws IOException {
+        try {
+            RestClient.builder((HttpHost[])null);
+            fail("should have failed");
+        } catch(IllegalArgumentException e) {
+            assertEquals("no hosts provided", e.getMessage());
+        }
+
+        try {
+            RestClient.builder();
+            fail("should have failed");
+        } catch(IllegalArgumentException e) {
+            assertEquals("no hosts provided", e.getMessage());
+        }
+
+        try {
+            RestClient.builder(new HttpHost[]{new HttpHost("localhost", 9200), null}).build();
+            fail("should have failed");
+        } catch(NullPointerException e) {
+            assertEquals("host cannot be null", e.getMessage());
+        }
+
+        try {
+            RestClient.builder(new HttpHost("localhost", 9200))
+                    .setMaxRetryTimeoutMillis(RandomInts.randomIntBetween(random(), Integer.MIN_VALUE, 0));
+            fail("should have failed");
+        } catch(IllegalArgumentException e) {
+            assertEquals("maxRetryTimeoutMillis must be greater than 0", e.getMessage());
+        }
+
+        try {
+            RestClient.builder(new HttpHost("localhost", 9200)).setDefaultHeaders(null);
+            fail("should have failed");
+        } catch(NullPointerException e) {
+            assertEquals("default headers must not be null", e.getMessage());
+        }
+
+        try {
+            RestClient.builder(new HttpHost("localhost", 9200)).setDefaultHeaders(new Header[]{null});
+            fail("should have failed");
+        } catch(NullPointerException e) {
+            assertEquals("default header must not be null", e.getMessage());
+        }
+
+        try {
+            RestClient.builder(new HttpHost("localhost", 9200)).setFailureListener(null);
+            fail("should have failed");
+        } catch(NullPointerException e) {
+            assertEquals("failure listener must not be null", e.getMessage());
+        }
+
+        int numNodes = RandomInts.randomIntBetween(random(), 1, 5);
+        HttpHost[] hosts = new HttpHost[numNodes];
+        for (int i = 0; i < numNodes; i++) {
+            hosts[i] = new HttpHost("localhost", 9200 + i);
+        }
+        RestClient.Builder builder = RestClient.builder(hosts);
+        if (random().nextBoolean()) {
+            builder.setHttpClient(HttpClientBuilder.create().build());
+        }
+        if (random().nextBoolean()) {
+            int numHeaders = RandomInts.randomIntBetween(random(), 1, 5);
+            Header[] headers = new Header[numHeaders];
+            for (int i = 0; i < numHeaders; i++) {
+                headers[i] = new BasicHeader("header" + i, "value");
+            }
+            builder.setDefaultHeaders(headers);
+        }
+        if (random().nextBoolean()) {
+            builder.setMaxRetryTimeoutMillis(RandomInts.randomIntBetween(random(), 1, Integer.MAX_VALUE));
+        }
+        try (RestClient restClient = builder.build()) {
+            assertNotNull(restClient);
+        }
+    }
+}

+ 218 - 0
client/src/test/java/org/elasticsearch/client/RestClientIntegTests.java

@@ -0,0 +1,218 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client;
+
+import com.carrotsearch.randomizedtesting.generators.RandomInts;
+import com.carrotsearch.randomizedtesting.generators.RandomStrings;
+import com.sun.net.httpserver.Headers;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+import org.apache.http.Consts;
+import org.apache.http.Header;
+import org.apache.http.HttpHost;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.message.BasicHeader;
+import org.apache.http.util.EntityUtils;
+import org.apache.lucene.util.LuceneTestCase;
+import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.elasticsearch.client.RestClientTestUtil.getAllStatusCodes;
+import static org.elasticsearch.client.RestClientTestUtil.getHttpMethods;
+import static org.elasticsearch.client.RestClientTestUtil.randomStatusCode;
+import static org.hamcrest.CoreMatchers.equalTo;
+
+/**
+ * Integration test to check interaction between {@link RestClient} and {@link org.apache.http.client.HttpClient}.
+ * Works against a real http server, one single host.
+ */
+//animal-sniffer doesn't like our usage of com.sun.net.httpserver.* classes
+@IgnoreJRERequirement
+public class RestClientIntegTests extends LuceneTestCase {
+
+    private static HttpServer httpServer;
+    private static RestClient restClient;
+    private static Header[] defaultHeaders;
+
+    @BeforeClass
+    public static void startHttpServer() throws Exception {
+        httpServer = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0), 0);
+        httpServer.start();
+        //returns a different status code depending on the path
+        for (int statusCode : getAllStatusCodes()) {
+            createStatusCodeContext(httpServer, statusCode);
+        }
+        int numHeaders = RandomInts.randomIntBetween(random(), 0, 3);
+        defaultHeaders = new Header[numHeaders];
+        for (int i = 0; i < numHeaders; i++) {
+            String headerName = "Header-default" + (random().nextBoolean() ? i : "");
+            String headerValue = RandomStrings.randomAsciiOfLengthBetween(random(), 3, 10);
+            defaultHeaders[i] = new BasicHeader(headerName, headerValue);
+        }
+        restClient = RestClient.builder(new HttpHost(httpServer.getAddress().getHostString(), httpServer.getAddress().getPort()))
+                .setDefaultHeaders(defaultHeaders).build();
+    }
+
+    private static void createStatusCodeContext(HttpServer httpServer, final int statusCode) {
+        httpServer.createContext("/" + statusCode, new ResponseHandler(statusCode));
+    }
+
+    //animal-sniffer doesn't like our usage of com.sun.net.httpserver.* classes
+    @IgnoreJRERequirement
+    private static class ResponseHandler implements HttpHandler {
+        private final int statusCode;
+
+        ResponseHandler(int statusCode) {
+            this.statusCode = statusCode;
+        }
+
+        @Override
+        public void handle(HttpExchange httpExchange) throws IOException {
+            StringBuilder body = new StringBuilder();
+            try (InputStreamReader reader = new InputStreamReader(httpExchange.getRequestBody(), Consts.UTF_8)) {
+                char[] buffer = new char[256];
+                int read;
+                while ((read = reader.read(buffer)) != -1) {
+                    body.append(buffer, 0, read);
+                }
+            }
+            Headers requestHeaders = httpExchange.getRequestHeaders();
+            Headers responseHeaders = httpExchange.getResponseHeaders();
+            for (Map.Entry<String, List<String>> header : requestHeaders.entrySet()) {
+                responseHeaders.put(header.getKey(), header.getValue());
+            }
+            httpExchange.getRequestBody().close();
+            httpExchange.sendResponseHeaders(statusCode, body.length() == 0 ? -1 : body.length());
+            if (body.length() > 0) {
+                try (OutputStream out = httpExchange.getResponseBody()) {
+                    out.write(body.toString().getBytes(Consts.UTF_8));
+                }
+            }
+            httpExchange.close();
+        }
+    }
+
+    @AfterClass
+    public static void stopHttpServers() throws IOException {
+        restClient.close();
+        restClient = null;
+        httpServer.stop(0);
+        httpServer = null;
+    }
+
+    /**
+     * End to end test for headers. We test it explicitly against a real http client as there are different ways
+     * to set/add headers to the {@link org.apache.http.client.HttpClient}.
+     * Exercises the test http server ability to send back whatever headers it received.
+     */
+    public void testHeaders() throws Exception {
+        for (String method : getHttpMethods()) {
+            Set<String> standardHeaders = new HashSet<>(
+                    Arrays.asList("Accept-encoding", "Connection", "Host", "User-agent", "Date"));
+            if (method.equals("HEAD") == false) {
+                standardHeaders.add("Content-length");
+            }
+            int numHeaders = RandomInts.randomIntBetween(random(), 1, 5);
+            Map<String, String> expectedHeaders = new HashMap<>();
+            for (Header defaultHeader : defaultHeaders) {
+                expectedHeaders.put(defaultHeader.getName(), defaultHeader.getValue());
+            }
+            Header[] headers = new Header[numHeaders];
+            for (int i = 0; i < numHeaders; i++) {
+                String headerName = "Header" + (random().nextBoolean() ? i : "");
+                String headerValue = RandomStrings.randomAsciiOfLengthBetween(random(), 3, 10);
+                headers[i] = new BasicHeader(headerName, headerValue);
+                expectedHeaders.put(headerName, headerValue);
+            }
+
+            int statusCode = randomStatusCode(random());
+            Response esResponse;
+            try (Response response = restClient.performRequest(method, "/" + statusCode,
+                    Collections.<String, String>emptyMap(), null, headers)) {
+                esResponse = response;
+            } catch(ResponseException e) {
+                esResponse = e.getResponse();
+            }
+            assertThat(esResponse.getStatusLine().getStatusCode(), equalTo(statusCode));
+            for (Header responseHeader : esResponse.getHeaders()) {
+                if (responseHeader.getName().startsWith("Header")) {
+                    String headerValue = expectedHeaders.remove(responseHeader.getName());
+                    assertNotNull("found response header [" + responseHeader.getName() + "] that wasn't originally sent", headerValue);
+                } else {
+                    assertTrue("unknown header was returned " + responseHeader.getName(),
+                            standardHeaders.remove(responseHeader.getName()));
+                }
+            }
+            assertEquals("some headers that were sent weren't returned: " + expectedHeaders, 0, expectedHeaders.size());
+            assertEquals("some expected standard headers weren't returned: " + standardHeaders, 0, standardHeaders.size());
+        }
+    }
+
+    /**
+     * End to end test for delete with body. We test it explicitly as it is not supported
+     * out of the box by {@link org.apache.http.client.HttpClient}.
+     * Exercises the test http server ability to send back whatever body it received.
+     */
+    public void testDeleteWithBody() throws Exception {
+        testBody("DELETE");
+    }
+
+    /**
+     * End to end test for get with body. We test it explicitly as it is not supported
+     * out of the box by {@link org.apache.http.client.HttpClient}.
+     * Exercises the test http server ability to send back whatever body it received.
+     */
+    public void testGetWithBody() throws Exception {
+        testBody("GET");
+    }
+
+    private void testBody(String method) throws Exception {
+        String requestBody = "{ \"field\": \"value\" }";
+        StringEntity entity = new StringEntity(requestBody);
+        Response esResponse;
+        String responseBody;
+        int statusCode = randomStatusCode(random());
+        try (Response response = restClient.performRequest(method, "/" + statusCode,
+                Collections.<String, String>emptyMap(), entity)) {
+            responseBody = EntityUtils.toString(response.getEntity());
+            esResponse = response;
+        } catch(ResponseException e) {
+            responseBody = e.getResponseBody();
+            esResponse = e.getResponse();
+        }
+        assertEquals(statusCode, esResponse.getStatusLine().getStatusCode());
+        assertEquals(requestBody, responseBody);
+    }
+}

+ 275 - 0
client/src/test/java/org/elasticsearch/client/RestClientMultipleHostsTests.java

@@ -0,0 +1,275 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client;
+
+import com.carrotsearch.randomizedtesting.generators.RandomInts;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpRequest;
+import org.apache.http.ProtocolVersion;
+import org.apache.http.StatusLine;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.conn.ConnectTimeoutException;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.message.BasicStatusLine;
+import org.apache.lucene.util.LuceneTestCase;
+import org.junit.Before;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.io.IOException;
+import java.net.SocketTimeoutException;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.elasticsearch.client.RestClientTestUtil.randomErrorNoRetryStatusCode;
+import static org.elasticsearch.client.RestClientTestUtil.randomErrorRetryStatusCode;
+import static org.elasticsearch.client.RestClientTestUtil.randomHttpMethod;
+import static org.elasticsearch.client.RestClientTestUtil.randomOkStatusCode;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for {@link RestClient} behaviour against multiple hosts: fail-over, blacklisting etc.
+ * Relies on a mock http client to intercept requests and return desired responses based on request path.
+ */
+public class RestClientMultipleHostsTests extends LuceneTestCase {
+
+    private RestClient restClient;
+    private HttpHost[] httpHosts;
+    private TrackingFailureListener failureListener;
+
+    @Before
+    public void createRestClient() throws IOException {
+        CloseableHttpClient httpClient = mock(CloseableHttpClient.class);
+        when(httpClient.execute(any(HttpHost.class), any(HttpRequest.class))).thenAnswer(new Answer<CloseableHttpResponse>() {
+            @Override
+            public CloseableHttpResponse answer(InvocationOnMock invocationOnMock) throws Throwable {
+                HttpHost httpHost = (HttpHost) invocationOnMock.getArguments()[0];
+                HttpUriRequest request = (HttpUriRequest) invocationOnMock.getArguments()[1];
+                //return the desired status code or exception depending on the path
+                if (request.getURI().getPath().equals("/soe")) {
+                    throw new SocketTimeoutException(httpHost.toString());
+                } else if (request.getURI().getPath().equals("/coe")) {
+                    throw new ConnectTimeoutException(httpHost.toString());
+                } else if (request.getURI().getPath().equals("/ioe")) {
+                    throw new IOException(httpHost.toString());
+                }
+                int statusCode = Integer.parseInt(request.getURI().getPath().substring(1));
+                StatusLine statusLine = new BasicStatusLine(new ProtocolVersion("http", 1, 1), statusCode, "");
+                return new CloseableBasicHttpResponse(statusLine);
+            }
+        });
+
+        int numHosts = RandomInts.randomIntBetween(random(), 2, 5);
+        httpHosts = new HttpHost[numHosts];
+        for (int i = 0; i < numHosts; i++) {
+            httpHosts[i] = new HttpHost("localhost", 9200 + i);
+        }
+        failureListener = new TrackingFailureListener();
+        restClient = RestClient.builder(httpHosts).setHttpClient(httpClient).setFailureListener(failureListener).build();
+    }
+
+    public void testRoundRobinOkStatusCodes() throws Exception {
+        int numIters = RandomInts.randomIntBetween(random(), 1, 5);
+        for (int i = 0; i < numIters; i++) {
+            Set<HttpHost> hostsSet = new HashSet<>();
+            Collections.addAll(hostsSet, httpHosts);
+            for (int j = 0; j < httpHosts.length; j++) {
+                int statusCode = randomOkStatusCode(random());
+                try (Response response = restClient.performRequest(randomHttpMethod(random()), "/" + statusCode,
+                        Collections.<String, String>emptyMap(), null)) {
+                    assertThat(response.getStatusLine().getStatusCode(), equalTo(statusCode));
+                    assertTrue("host not found: " + response.getHost(), hostsSet.remove(response.getHost()));
+                }
+            }
+            assertEquals("every host should have been used but some weren't: " + hostsSet, 0, hostsSet.size());
+        }
+        failureListener.assertNotCalled();
+    }
+
+    public void testRoundRobinNoRetryErrors() throws Exception {
+        int numIters = RandomInts.randomIntBetween(random(), 1, 5);
+        for (int i = 0; i < numIters; i++) {
+            Set<HttpHost> hostsSet = new HashSet<>();
+            Collections.addAll(hostsSet, httpHosts);
+            for (int j = 0; j < httpHosts.length; j++) {
+                String method = randomHttpMethod(random());
+                int statusCode = randomErrorNoRetryStatusCode(random());
+                try (Response response = restClient.performRequest(method, "/" + statusCode,
+                        Collections.<String, String>emptyMap(), null)) {
+                    if (method.equals("HEAD") && statusCode == 404) {
+                        //no exception gets thrown although we got a 404
+                        assertThat(response.getStatusLine().getStatusCode(), equalTo(404));
+                        assertThat(response.getStatusLine().getStatusCode(), equalTo(statusCode));
+                        assertTrue("host not found: " + response.getHost(), hostsSet.remove(response.getHost()));
+                    } else {
+                        fail("request should have failed");
+                    }
+                } catch(ResponseException e) {
+                    if (method.equals("HEAD") && statusCode == 404) {
+                        throw e;
+                    }
+                    Response response = e.getResponse();
+                    assertThat(response.getStatusLine().getStatusCode(), equalTo(statusCode));
+                    assertTrue("host not found: " + response.getHost(), hostsSet.remove(response.getHost()));
+                    assertEquals(0, e.getSuppressed().length);
+                }
+            }
+            assertEquals("every host should have been used but some weren't: " + hostsSet, 0, hostsSet.size());
+        }
+        failureListener.assertNotCalled();
+    }
+
+    public void testRoundRobinRetryErrors() throws Exception {
+        String retryEndpoint = randomErrorRetryEndpoint();
+        try  {
+            restClient.performRequest(randomHttpMethod(random()), retryEndpoint, Collections.<String, String>emptyMap(), null);
+            fail("request should have failed");
+        } catch(ResponseException e) {
+            Set<HttpHost> hostsSet = new HashSet<>();
+            Collections.addAll(hostsSet, httpHosts);
+            //first request causes all the hosts to be blacklisted, the returned exception holds one suppressed exception each
+            failureListener.assertCalled(httpHosts);
+            do {
+                Response response = e.getResponse();
+                assertThat(response.getStatusLine().getStatusCode(), equalTo(Integer.parseInt(retryEndpoint.substring(1))));
+                assertTrue("host [" + response.getHost() + "] not found, most likely used multiple times",
+                        hostsSet.remove(response.getHost()));
+                if (e.getSuppressed().length > 0) {
+                    assertEquals(1, e.getSuppressed().length);
+                    Throwable suppressed = e.getSuppressed()[0];
+                    assertThat(suppressed, instanceOf(ResponseException.class));
+                    e = (ResponseException)suppressed;
+                } else {
+                    e = null;
+                }
+            } while(e != null);
+            assertEquals("every host should have been used but some weren't: " + hostsSet, 0, hostsSet.size());
+        } catch(IOException e) {
+            Set<HttpHost> hostsSet = new HashSet<>();
+            Collections.addAll(hostsSet, httpHosts);
+            //first request causes all the hosts to be blacklisted, the returned exception holds one suppressed exception each
+            failureListener.assertCalled(httpHosts);
+            do {
+                HttpHost httpHost = HttpHost.create(e.getMessage());
+                assertTrue("host [" + httpHost + "] not found, most likely used multiple times", hostsSet.remove(httpHost));
+                if (e.getSuppressed().length > 0) {
+                    assertEquals(1, e.getSuppressed().length);
+                    Throwable suppressed = e.getSuppressed()[0];
+                    assertThat(suppressed, instanceOf(IOException.class));
+                    e = (IOException) suppressed;
+                } else {
+                    e = null;
+                }
+            } while(e != null);
+            assertEquals("every host should have been used but some weren't: " + hostsSet, 0, hostsSet.size());
+        }
+
+        int numIters = RandomInts.randomIntBetween(random(), 2, 5);
+        for (int i = 1; i <= numIters; i++) {
+            //check that one different host is resurrected at each new attempt
+            Set<HttpHost> hostsSet = new HashSet<>();
+            Collections.addAll(hostsSet, httpHosts);
+            for (int j = 0; j < httpHosts.length; j++) {
+                retryEndpoint = randomErrorRetryEndpoint();
+                try  {
+                    restClient.performRequest(randomHttpMethod(random()), retryEndpoint, Collections.<String, String>emptyMap(), null);
+                    fail("request should have failed");
+                } catch(ResponseException e) {
+                    Response response = e.getResponse();
+                    assertThat(response.getStatusLine().getStatusCode(), equalTo(Integer.parseInt(retryEndpoint.substring(1))));
+                    assertTrue("host [" + response.getHost() + "] not found, most likely used multiple times",
+                            hostsSet.remove(response.getHost()));
+                    //after the first request, all hosts are blacklisted, a single one gets resurrected each time
+                    failureListener.assertCalled(response.getHost());
+                    assertEquals(0, e.getSuppressed().length);
+                } catch(IOException e) {
+                    HttpHost httpHost = HttpHost.create(e.getMessage());
+                    assertTrue("host [" + httpHost + "] not found, most likely used multiple times", hostsSet.remove(httpHost));
+                    //after the first request, all hosts are blacklisted, a single one gets resurrected each time
+                    failureListener.assertCalled(httpHost);
+                    assertEquals(0, e.getSuppressed().length);
+                }
+            }
+            assertEquals("every host should have been used but some weren't: " + hostsSet, 0, hostsSet.size());
+            if (random().nextBoolean()) {
+                //mark one host back alive through a successful request and check that all requests after that are sent to it
+                HttpHost selectedHost = null;
+                int iters = RandomInts.randomIntBetween(random(), 2, 10);
+                for (int y = 0; y < iters; y++) {
+                    int statusCode = randomErrorNoRetryStatusCode(random());
+                    Response response;
+                    try (Response esResponse = restClient.performRequest(randomHttpMethod(random()), "/" + statusCode,
+                            Collections.<String, String>emptyMap(), null)) {
+                        response = esResponse;
+                    }
+                    catch(ResponseException e) {
+                        response = e.getResponse();
+                    }
+                    assertThat(response.getStatusLine().getStatusCode(), equalTo(statusCode));
+                    if (selectedHost == null) {
+                        selectedHost = response.getHost();
+                    } else {
+                        assertThat(response.getHost(), equalTo(selectedHost));
+                    }
+                }
+                failureListener.assertNotCalled();
+                //let the selected host catch up on number of failures, it gets selected a consecutive number of times as it's the one
+                //selected to be retried earlier (due to lower number of failures) till all the hosts have the same number of failures
+                for (int y = 0; y < i + 1; y++) {
+                    retryEndpoint = randomErrorRetryEndpoint();
+                    try {
+                        restClient.performRequest(randomHttpMethod(random()), retryEndpoint,
+                                Collections.<String, String>emptyMap(), null);
+                        fail("request should have failed");
+                    } catch(ResponseException e) {
+                        Response response = e.getResponse();
+                        assertThat(response.getStatusLine().getStatusCode(), equalTo(Integer.parseInt(retryEndpoint.substring(1))));
+                        assertThat(response.getHost(), equalTo(selectedHost));
+                        failureListener.assertCalled(selectedHost);
+                    } catch(IOException e) {
+                        HttpHost httpHost = HttpHost.create(e.getMessage());
+                        assertThat(httpHost, equalTo(selectedHost));
+                        failureListener.assertCalled(selectedHost);
+                    }
+                }
+            }
+        }
+    }
+
+    private static String randomErrorRetryEndpoint() {
+        switch(RandomInts.randomIntBetween(random(), 0, 3)) {
+            case 0:
+                return "/" + randomErrorRetryStatusCode(random());
+            case 1:
+                return "/coe";
+            case 2:
+                return "/soe";
+            case 3:
+                return "/ioe";
+        }
+        throw new UnsupportedOperationException();
+    }
+}

+ 421 - 0
client/src/test/java/org/elasticsearch/client/RestClientSingleHostTests.java

@@ -0,0 +1,421 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client;
+
+import com.carrotsearch.randomizedtesting.generators.RandomInts;
+import com.carrotsearch.randomizedtesting.generators.RandomStrings;
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpEntityEnclosingRequest;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpRequest;
+import org.apache.http.ProtocolVersion;
+import org.apache.http.StatusLine;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpHead;
+import org.apache.http.client.methods.HttpOptions;
+import org.apache.http.client.methods.HttpPatch;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.client.methods.HttpTrace;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.utils.URIBuilder;
+import org.apache.http.conn.ConnectTimeoutException;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.message.BasicHeader;
+import org.apache.http.message.BasicStatusLine;
+import org.apache.http.util.EntityUtils;
+import org.apache.lucene.util.LuceneTestCase;
+import org.junit.Before;
+import org.mockito.ArgumentCaptor;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.io.IOException;
+import java.net.SocketTimeoutException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.elasticsearch.client.RestClientTestUtil.getAllErrorStatusCodes;
+import static org.elasticsearch.client.RestClientTestUtil.getHttpMethods;
+import static org.elasticsearch.client.RestClientTestUtil.getOkStatusCodes;
+import static org.elasticsearch.client.RestClientTestUtil.randomHttpMethod;
+import static org.elasticsearch.client.RestClientTestUtil.randomStatusCode;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for basic functionality of {@link RestClient} against one single host: tests http requests being sent, headers,
+ * body, different status codes and corresponding responses/exceptions.
+ * Relies on a mock http client to intercept requests and return desired responses based on request path.
+ */
+public class RestClientSingleHostTests extends LuceneTestCase {
+
+    private RestClient restClient;
+    private Header[] defaultHeaders;
+    private HttpHost httpHost;
+    private CloseableHttpClient httpClient;
+    private TrackingFailureListener failureListener;
+
+    @Before
+    public void createRestClient() throws IOException {
+        httpClient = mock(CloseableHttpClient.class);
+        when(httpClient.execute(any(HttpHost.class), any(HttpRequest.class))).thenAnswer(new Answer<CloseableHttpResponse>() {
+            @Override
+            public CloseableHttpResponse answer(InvocationOnMock invocationOnMock) throws Throwable {
+                HttpUriRequest request = (HttpUriRequest) invocationOnMock.getArguments()[1];
+                //return the desired status code or exception depending on the path
+                if (request.getURI().getPath().equals("/soe")) {
+                    throw new SocketTimeoutException();
+                } else if (request.getURI().getPath().equals("/coe")) {
+                    throw new ConnectTimeoutException();
+                }
+                int statusCode = Integer.parseInt(request.getURI().getPath().substring(1));
+                StatusLine statusLine = new BasicStatusLine(new ProtocolVersion("http", 1, 1), statusCode, "");
+
+                CloseableHttpResponse httpResponse = new CloseableBasicHttpResponse(statusLine);
+                //return the same body that was sent
+                if (request instanceof HttpEntityEnclosingRequest) {
+                    HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
+                    if (entity != null) {
+                        assertTrue("the entity is not repeatable, cannot set it to the response directly", entity.isRepeatable());
+                        httpResponse.setEntity(entity);
+                    }
+                }
+                //return the same headers that were sent
+                httpResponse.setHeaders(request.getAllHeaders());
+                return httpResponse;
+            }
+        });
+        int numHeaders = RandomInts.randomIntBetween(random(), 0, 3);
+        defaultHeaders = new Header[numHeaders];
+        for (int i = 0; i < numHeaders; i++) {
+            String headerName = "Header-default" + (random().nextBoolean() ? i : "");
+            String headerValue = RandomStrings.randomAsciiOfLengthBetween(random(), 3, 10);
+            defaultHeaders[i] = new BasicHeader(headerName, headerValue);
+        }
+        httpHost = new HttpHost("localhost", 9200);
+        failureListener = new TrackingFailureListener();
+        restClient = RestClient.builder(httpHost).setHttpClient(httpClient).setDefaultHeaders(defaultHeaders)
+                .setFailureListener(failureListener).build();
+    }
+
+    /**
+     * Verifies the content of the {@link HttpRequest} that's internally created and passed through to the http client
+     */
+    public void testInternalHttpRequest() throws Exception {
+        ArgumentCaptor<HttpUriRequest> requestArgumentCaptor = ArgumentCaptor.forClass(HttpUriRequest.class);
+        int times = 0;
+        for (String httpMethod : getHttpMethods()) {
+            HttpUriRequest expectedRequest = performRandomRequest(httpMethod);
+            verify(httpClient, times(++times)).execute(any(HttpHost.class), requestArgumentCaptor.capture());
+            HttpUriRequest actualRequest = requestArgumentCaptor.getValue();
+            assertEquals(expectedRequest.getURI(), actualRequest.getURI());
+            assertEquals(expectedRequest.getClass(), actualRequest.getClass());
+            assertArrayEquals(expectedRequest.getAllHeaders(), actualRequest.getAllHeaders());
+            if (expectedRequest instanceof HttpEntityEnclosingRequest) {
+                HttpEntity expectedEntity = ((HttpEntityEnclosingRequest) expectedRequest).getEntity();
+                if (expectedEntity != null) {
+                    HttpEntity actualEntity = ((HttpEntityEnclosingRequest) actualRequest).getEntity();
+                    assertEquals(EntityUtils.toString(expectedEntity), EntityUtils.toString(actualEntity));
+                }
+            }
+        }
+    }
+
+    public void testSetNodes() throws IOException {
+        try {
+            restClient.setHosts((HttpHost[]) null);
+            fail("setHosts should have failed");
+        } catch (IllegalArgumentException e) {
+            assertEquals("hosts must not be null nor empty", e.getMessage());
+        }
+        try {
+            restClient.setHosts();
+            fail("setHosts should have failed");
+        } catch (IllegalArgumentException e) {
+            assertEquals("hosts must not be null nor empty", e.getMessage());
+        }
+        try {
+            restClient.setHosts((HttpHost) null);
+            fail("setHosts should have failed");
+        } catch (NullPointerException e) {
+            assertEquals("host cannot be null", e.getMessage());
+        }
+        try {
+            restClient.setHosts(new HttpHost("localhost", 9200), null, new HttpHost("localhost", 9201));
+            fail("setHosts should have failed");
+        } catch (NullPointerException e) {
+            assertEquals("host cannot be null", e.getMessage());
+        }
+    }
+
+    /**
+     * End to end test for ok status codes
+     */
+    public void testOkStatusCodes() throws Exception {
+        for (String method : getHttpMethods()) {
+            for (int okStatusCode : getOkStatusCodes()) {
+                Response response = restClient.performRequest(method, "/" + okStatusCode,
+                        Collections.<String, String>emptyMap(), null);
+                assertThat(response.getStatusLine().getStatusCode(), equalTo(okStatusCode));
+            }
+        }
+        failureListener.assertNotCalled();
+    }
+
+    /**
+     * End to end test for error status codes: they should cause an exception to be thrown, apart from 404 with HEAD requests
+     */
+    public void testErrorStatusCodes() throws Exception {
+        for (String method : getHttpMethods()) {
+            //error status codes should cause an exception to be thrown
+            for (int errorStatusCode : getAllErrorStatusCodes()) {
+                try (Response response = restClient.performRequest(method, "/" + errorStatusCode,
+                        Collections.<String, String>emptyMap(), null)) {
+                    if (method.equals("HEAD") && errorStatusCode == 404) {
+                        //no exception gets thrown although we got a 404
+                        assertThat(response.getStatusLine().getStatusCode(), equalTo(errorStatusCode));
+                    } else {
+                        fail("request should have failed");
+                    }
+                } catch(ResponseException e) {
+                    if (method.equals("HEAD") && errorStatusCode == 404) {
+                        throw e;
+                    }
+                    assertThat(e.getResponse().getStatusLine().getStatusCode(), equalTo(errorStatusCode));
+                }
+                if (errorStatusCode <= 500) {
+                    failureListener.assertNotCalled();
+                } else {
+                    failureListener.assertCalled(httpHost);
+                }
+            }
+        }
+    }
+
+    public void testIOExceptions() throws IOException {
+        for (String method : getHttpMethods()) {
+            //IOExceptions should be let bubble up
+            try {
+                restClient.performRequest(method, "/coe", Collections.<String, String>emptyMap(), null);
+                fail("request should have failed");
+            } catch(IOException e) {
+                assertThat(e, instanceOf(ConnectTimeoutException.class));
+            }
+            failureListener.assertCalled(httpHost);
+            try {
+                restClient.performRequest(method, "/soe", Collections.<String, String>emptyMap(), null);
+                fail("request should have failed");
+            } catch(IOException e) {
+                assertThat(e, instanceOf(SocketTimeoutException.class));
+            }
+            failureListener.assertCalled(httpHost);
+        }
+    }
+
+    /**
+     * End to end test for request and response body. Exercises the mock http client ability to send back
+     * whatever body it has received.
+     */
+    public void testBody() throws Exception {
+        String body = "{ \"field\": \"value\" }";
+        StringEntity entity = new StringEntity(body);
+        for (String method : Arrays.asList("DELETE", "GET", "PATCH", "POST", "PUT")) {
+            for (int okStatusCode : getOkStatusCodes()) {
+                try (Response response = restClient.performRequest(method, "/" + okStatusCode,
+                        Collections.<String, String>emptyMap(), entity)) {
+                    assertThat(response.getStatusLine().getStatusCode(), equalTo(okStatusCode));
+                    assertThat(EntityUtils.toString(response.getEntity()), equalTo(body));
+                }
+            }
+            for (int errorStatusCode : getAllErrorStatusCodes()) {
+                try {
+                    restClient.performRequest(method, "/" + errorStatusCode, Collections.<String, String>emptyMap(), entity);
+                    fail("request should have failed");
+                } catch(ResponseException e) {
+                    Response response = e.getResponse();
+                    assertThat(response.getStatusLine().getStatusCode(), equalTo(errorStatusCode));
+                    assertThat(EntityUtils.toString(response.getEntity()), equalTo(body));
+                }
+            }
+        }
+        for (String method : Arrays.asList("HEAD", "OPTIONS", "TRACE")) {
+            try {
+                restClient.performRequest(method, "/" + randomStatusCode(random()),
+                        Collections.<String, String>emptyMap(), entity);
+                fail("request should have failed");
+            } catch(UnsupportedOperationException e) {
+                assertThat(e.getMessage(), equalTo(method + " with body is not supported"));
+            }
+        }
+    }
+
+    public void testNullHeaders() throws Exception {
+        String method = randomHttpMethod(random());
+        int statusCode = randomStatusCode(random());
+        try {
+            restClient.performRequest(method, "/" + statusCode, Collections.<String, String>emptyMap(), null, (Header[])null);
+            fail("request should have failed");
+        } catch(NullPointerException e) {
+            assertEquals("request headers must not be null", e.getMessage());
+        }
+        try {
+            restClient.performRequest(method, "/" + statusCode, Collections.<String, String>emptyMap(), null, (Header)null);
+            fail("request should have failed");
+        } catch(NullPointerException e) {
+            assertEquals("request header must not be null", e.getMessage());
+        }
+    }
+
+    public void testNullParams() throws Exception {
+        String method = randomHttpMethod(random());
+        int statusCode = randomStatusCode(random());
+        try {
+            restClient.performRequest(method, "/" + statusCode, null, null);
+            fail("request should have failed");
+        } catch(NullPointerException e) {
+            assertEquals("params must not be null", e.getMessage());
+        }
+    }
+
+    /**
+     * End to end test for request and response headers. Exercises the mock http client ability to send back
+     * whatever headers it has received.
+     */
+    public void testHeaders() throws Exception {
+        for (String method : getHttpMethods()) {
+            Map<String, String> expectedHeaders = new HashMap<>();
+            for (Header defaultHeader : defaultHeaders) {
+                expectedHeaders.put(defaultHeader.getName(), defaultHeader.getValue());
+            }
+            int numHeaders = RandomInts.randomIntBetween(random(), 1, 5);
+            Header[] headers = new Header[numHeaders];
+            for (int i = 0; i < numHeaders; i++) {
+                String headerName = "Header" + (random().nextBoolean() ? i : "");
+                String headerValue = RandomStrings.randomAsciiOfLengthBetween(random(), 3, 10);
+                headers[i] = new BasicHeader(headerName, headerValue);
+                expectedHeaders.put(headerName, headerValue);
+            }
+
+            int statusCode = randomStatusCode(random());
+            Response esResponse;
+            try (Response response = restClient.performRequest(method, "/" + statusCode,
+                    Collections.<String, String>emptyMap(), null, headers)) {
+                esResponse = response;
+            } catch(ResponseException e) {
+                esResponse = e.getResponse();
+            }
+            assertThat(esResponse.getStatusLine().getStatusCode(), equalTo(statusCode));
+            for (Header responseHeader : esResponse.getHeaders()) {
+                String headerValue = expectedHeaders.remove(responseHeader.getName());
+                assertNotNull("found response header [" + responseHeader.getName() + "] that wasn't originally sent", headerValue);
+            }
+            assertEquals("some headers that were sent weren't returned " + expectedHeaders, 0, expectedHeaders.size());
+        }
+    }
+
+    private HttpUriRequest performRandomRequest(String method) throws IOException, URISyntaxException {
+        String uriAsString = "/" + randomStatusCode(random());
+        URIBuilder uriBuilder = new URIBuilder(uriAsString);
+        Map<String, String> params = Collections.emptyMap();
+        if (random().nextBoolean()) {
+            int numParams = RandomInts.randomIntBetween(random(), 1, 3);
+            params = new HashMap<>(numParams);
+            for (int i = 0; i < numParams; i++) {
+                String paramKey = "param-" + i;
+                String paramValue = RandomStrings.randomAsciiOfLengthBetween(random(), 3, 10);
+                params.put(paramKey, paramValue);
+                uriBuilder.addParameter(paramKey, paramValue);
+            }
+        }
+        URI uri = uriBuilder.build();
+
+        HttpUriRequest request;
+        switch(method) {
+            case "DELETE":
+                request = new HttpDeleteWithEntity(uri);
+                break;
+            case "GET":
+                request = new HttpGetWithEntity(uri);
+                break;
+            case "HEAD":
+                request = new HttpHead(uri);
+                break;
+            case "OPTIONS":
+                request = new HttpOptions(uri);
+                break;
+            case "PATCH":
+                request = new HttpPatch(uri);
+                break;
+            case "POST":
+                request = new HttpPost(uri);
+                break;
+            case "PUT":
+                request = new HttpPut(uri);
+                break;
+            case "TRACE":
+                request = new HttpTrace(uri);
+                break;
+            default:
+                throw new UnsupportedOperationException("method not supported: " + method);
+        }
+
+        HttpEntity entity = null;
+        if (request instanceof HttpEntityEnclosingRequest && random().nextBoolean()) {
+            entity = new StringEntity(RandomStrings.randomAsciiOfLengthBetween(random(), 10, 100));
+            ((HttpEntityEnclosingRequest) request).setEntity(entity);
+        }
+
+        Header[] headers = new Header[0];
+        for (Header defaultHeader : defaultHeaders) {
+            //default headers are expected but not sent for each request
+            request.setHeader(defaultHeader);
+        }
+        if (random().nextBoolean()) {
+            int numHeaders = RandomInts.randomIntBetween(random(), 1, 5);
+            headers = new Header[numHeaders];
+            for (int i = 0; i < numHeaders; i++) {
+                String headerName = "Header" + (random().nextBoolean() ? i : "");
+                String headerValue = RandomStrings.randomAsciiOfLengthBetween(random(), 3, 10);
+                BasicHeader basicHeader = new BasicHeader(headerName, headerValue);
+                headers[i] = basicHeader;
+                request.setHeader(basicHeader);
+            }
+        }
+
+        try {
+            restClient.performRequest(method, uriAsString, params, entity, headers);
+        } catch(ResponseException e) {
+            //all good
+        }
+        return request;
+    }
+}

+ 84 - 0
client/src/test/java/org/elasticsearch/client/RestClientTestUtil.java

@@ -0,0 +1,84 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client;
+
+import com.carrotsearch.randomizedtesting.generators.RandomPicks;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+
+final class RestClientTestUtil {
+
+    private static final String[] HTTP_METHODS = new String[]{"DELETE", "HEAD", "GET", "OPTIONS", "PATCH", "POST", "PUT", "TRACE"};
+    private static final List<Integer> ALL_STATUS_CODES;
+    private static final List<Integer> OK_STATUS_CODES = Arrays.asList(200, 201);
+    private static final List<Integer> ALL_ERROR_STATUS_CODES;
+    private static List<Integer> ERROR_NO_RETRY_STATUS_CODES = Arrays.asList(400, 401, 403, 404, 405, 500);
+    private static List<Integer> ERROR_RETRY_STATUS_CODES = Arrays.asList(502, 503, 504);
+
+    static {
+        ALL_ERROR_STATUS_CODES = new ArrayList<>(ERROR_RETRY_STATUS_CODES);
+        ALL_ERROR_STATUS_CODES.addAll(ERROR_NO_RETRY_STATUS_CODES);
+        ALL_STATUS_CODES = new ArrayList<>(ALL_ERROR_STATUS_CODES);
+        ALL_STATUS_CODES.addAll(OK_STATUS_CODES);
+    }
+
+    private RestClientTestUtil() {
+
+    }
+
+    static String[] getHttpMethods() {
+        return HTTP_METHODS;
+    }
+
+    static String randomHttpMethod(Random random) {
+        return RandomPicks.randomFrom(random, HTTP_METHODS);
+    }
+
+    static int randomStatusCode(Random random) {
+        return RandomPicks.randomFrom(random, ALL_ERROR_STATUS_CODES);
+    }
+
+    static int randomOkStatusCode(Random random) {
+        return RandomPicks.randomFrom(random, OK_STATUS_CODES);
+    }
+
+    static int randomErrorNoRetryStatusCode(Random random) {
+        return RandomPicks.randomFrom(random, ERROR_NO_RETRY_STATUS_CODES);
+    }
+
+    static int randomErrorRetryStatusCode(Random random) {
+        return RandomPicks.randomFrom(random, ERROR_RETRY_STATUS_CODES);
+    }
+
+    static List<Integer> getOkStatusCodes() {
+        return OK_STATUS_CODES;
+    }
+
+    static List<Integer> getAllErrorStatusCodes() {
+        return ALL_ERROR_STATUS_CODES;
+    }
+
+    static List<Integer> getAllStatusCodes() {
+        return ALL_STATUS_CODES;
+    }
+}

+ 52 - 0
client/src/test/java/org/elasticsearch/client/TrackingFailureListener.java

@@ -0,0 +1,52 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client;
+
+import org.apache.http.HttpHost;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+/**
+ * {@link org.elasticsearch.client.RestClient.FailureListener} impl that allows to track when it gets called
+ */
+class TrackingFailureListener extends RestClient.FailureListener {
+    private Set<HttpHost> hosts = new HashSet<>();
+
+    @Override
+    public void onFailure(HttpHost host) throws IOException {
+        hosts.add(host);
+    }
+
+    void assertCalled(HttpHost... hosts) {
+        assertEquals(hosts.length, this.hosts.size());
+        assertThat(this.hosts, containsInAnyOrder(hosts));
+        this.hosts.clear();
+    }
+
+    void assertNotCalled() {
+        assertEquals(0, hosts.size());
+    }
+}

+ 0 - 1
core/src/test/java/org/elasticsearch/ExceptionSerializationTests.java

@@ -121,7 +121,6 @@ public class ExceptionSerializationTests extends ESTestCase {
                 .resolve("org").resolve("elasticsearch");
         final Set<? extends Class<?>> ignore = Sets.newHashSet(
                 org.elasticsearch.test.rest.parser.RestTestParseException.class,
-                org.elasticsearch.test.rest.client.RestException.class,
                 CancellableThreadsTests.CustomException.class,
                 org.elasticsearch.rest.BytesRestResponseTests.WithHeadersException.class,
                 AbstractClientHeadersTestCase.InternalException.class);

+ 38 - 50
core/src/test/java/org/elasticsearch/http/netty/NettyHttpCompressionIT.java

@@ -22,27 +22,31 @@ import org.apache.http.Header;
 import org.apache.http.HttpException;
 import org.apache.http.HttpHeaders;
 import org.apache.http.HttpResponseInterceptor;
+import org.apache.http.entity.StringEntity;
 import org.apache.http.impl.client.CloseableHttpClient;
 import org.apache.http.impl.client.HttpClients;
+import org.apache.http.message.BasicHeader;
 import org.apache.http.protocol.HttpContext;
+import org.elasticsearch.client.Response;
+import org.elasticsearch.client.RestClient;
 import org.elasticsearch.common.network.NetworkModule;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.http.HttpTransportSettings;
 import org.elasticsearch.test.ESIntegTestCase;
-import org.elasticsearch.test.rest.client.http.HttpResponse;
 
 import java.io.IOException;
+import java.util.Collections;
 
 @ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.SUITE, numDataNodes = 1, numClientNodes = 1)
 public class NettyHttpCompressionIT extends ESIntegTestCase {
     private static final String GZIP_ENCODING = "gzip";
 
-    private static final String SAMPLE_DOCUMENT = "{\n" +
+    private static final StringEntity SAMPLE_DOCUMENT = new StringEntity("{\n" +
         "   \"name\": {\n" +
         "      \"first name\": \"Steve\",\n" +
         "      \"last name\": \"Jobs\"\n" +
         "   }\n" +
-        "}";
+        "}", RestClient.JSON_CONTENT_TYPE);
 
     @Override
     protected Settings nodeSettings(int nodeOrdinal) {
@@ -55,69 +59,55 @@ public class NettyHttpCompressionIT extends ESIntegTestCase {
 
     public void testCompressesResponseIfRequested() throws Exception {
         ensureGreen();
-
         // we need to intercept early, otherwise internal logic in HttpClient will just remove the header and we cannot verify it
         ContentEncodingHeaderExtractor headerExtractor = new ContentEncodingHeaderExtractor();
-        CloseableHttpClient internalClient = HttpClients.custom().addInterceptorFirst(headerExtractor).build();
-
-        HttpResponse response = httpClient(internalClient).path("/").addHeader(HttpHeaders.ACCEPT_ENCODING, GZIP_ENCODING).execute();
-        assertEquals(200, response.getStatusCode());
-        assertTrue(headerExtractor.hasContentEncodingHeader());
-        assertEquals(GZIP_ENCODING, headerExtractor.getContentEncodingHeader().getValue());
+        try (RestClient client = createRestClient(HttpClients.custom().addInterceptorFirst(headerExtractor).build())) {
+            try (Response response = client.performRequest("GET", "/", Collections.emptyMap(), null,
+                    new BasicHeader(HttpHeaders.ACCEPT_ENCODING, GZIP_ENCODING))) {
+                assertEquals(200, response.getStatusLine().getStatusCode());
+                assertTrue(headerExtractor.hasContentEncodingHeader());
+                assertEquals(GZIP_ENCODING, headerExtractor.getContentEncodingHeader().getValue());
+            }
+        }
     }
 
     public void testUncompressedResponseByDefault() throws Exception {
         ensureGreen();
-
         ContentEncodingHeaderExtractor headerExtractor = new ContentEncodingHeaderExtractor();
-        CloseableHttpClient internalClient = HttpClients
-            .custom()
-            .disableContentCompression()
-            .addInterceptorFirst(headerExtractor)
-            .build();
-
-        HttpResponse response = httpClient(internalClient).path("/").execute();
-        assertEquals(200, response.getStatusCode());
-        assertFalse(headerExtractor.hasContentEncodingHeader());
+        CloseableHttpClient httpClient = HttpClients.custom().disableContentCompression().addInterceptorFirst(headerExtractor).build();
+        try (RestClient client = createRestClient(httpClient)) {
+            try (Response response = client.performRequest("GET", "/", Collections.emptyMap(), null)) {
+                assertEquals(200, response.getStatusLine().getStatusCode());
+                assertFalse(headerExtractor.hasContentEncodingHeader());
+            }
+        }
     }
 
     public void testCanInterpretUncompressedRequest() throws Exception {
         ensureGreen();
-
         ContentEncodingHeaderExtractor headerExtractor = new ContentEncodingHeaderExtractor();
-        CloseableHttpClient internalClient = HttpClients
-            .custom()
-            // this disable content compression in both directions (request and response)
-            .disableContentCompression()
-            .addInterceptorFirst(headerExtractor)
-            .build();
-
-        HttpResponse response = httpClient(internalClient)
-            .path("/company/employees/1")
-            .method("POST")
-            .body(SAMPLE_DOCUMENT)
-            .execute();
-
-        assertEquals(201, response.getStatusCode());
-        assertFalse(headerExtractor.hasContentEncodingHeader());
+        // this disable content compression in both directions (request and response)
+        CloseableHttpClient httpClient = HttpClients.custom().disableContentCompression().addInterceptorFirst(headerExtractor).build();
+        try (RestClient client = createRestClient(httpClient)) {
+            try (Response response = client.performRequest("POST", "/company/employees/1",
+                    Collections.emptyMap(), SAMPLE_DOCUMENT)) {
+                assertEquals(201, response.getStatusLine().getStatusCode());
+                assertFalse(headerExtractor.hasContentEncodingHeader());
+            }
+        }
     }
 
     public void testCanInterpretCompressedRequest() throws Exception {
         ensureGreen();
-
         ContentEncodingHeaderExtractor headerExtractor = new ContentEncodingHeaderExtractor();
         // we don't call #disableContentCompression() hence the client will send the content compressed
-        CloseableHttpClient internalClient = HttpClients.custom().addInterceptorFirst(headerExtractor).build();
-
-        HttpResponse response = httpClient(internalClient)
-            .path("/company/employees/2")
-            .method("POST")
-            .body(SAMPLE_DOCUMENT)
-            .execute();
-
-        assertEquals(201, response.getStatusCode());
-        assertTrue(headerExtractor.hasContentEncodingHeader());
-        assertEquals(GZIP_ENCODING, headerExtractor.getContentEncodingHeader().getValue());
+        try (RestClient client = createRestClient(HttpClients.custom().addInterceptorFirst(headerExtractor).build())) {
+            try (Response response = client.performRequest("POST", "/company/employees/2",
+                    Collections.emptyMap(), SAMPLE_DOCUMENT)) {
+                assertEquals(201, response.getStatusLine().getStatusCode());
+                assertEquals(GZIP_ENCODING, headerExtractor.getContentEncodingHeader().getValue());
+            }
+        }
     }
 
     private static class ContentEncodingHeaderExtractor implements HttpResponseInterceptor {
@@ -141,6 +131,4 @@ public class NettyHttpCompressionIT extends ESIntegTestCase {
             return contentEncodingHeader;
         }
     }
-
-
 }

+ 13 - 15
core/src/test/java/org/elasticsearch/options/detailederrors/DetailedErrorsDisabledIT.java

@@ -19,17 +19,16 @@
 
 package org.elasticsearch.options.detailederrors;
 
-import org.apache.http.impl.client.HttpClients;
+import org.elasticsearch.client.Response;
+import org.elasticsearch.client.ResponseException;
 import org.elasticsearch.common.network.NetworkModule;
 import org.elasticsearch.common.settings.Settings;
-import org.elasticsearch.http.HttpServerTransport;
 import org.elasticsearch.http.HttpTransportSettings;
 import org.elasticsearch.test.ESIntegTestCase;
 import org.elasticsearch.test.ESIntegTestCase.ClusterScope;
 import org.elasticsearch.test.ESIntegTestCase.Scope;
-import org.elasticsearch.test.rest.client.http.HttpDeleteWithEntity;
-import org.elasticsearch.test.rest.client.http.HttpRequestBuilder;
-import org.elasticsearch.test.rest.client.http.HttpResponse;
+
+import java.util.Collections;
 
 import static org.hamcrest.Matchers.is;
 
@@ -49,15 +48,14 @@ public class DetailedErrorsDisabledIT extends ESIntegTestCase {
     }
 
     public void testThatErrorTraceParamReturns400() throws Exception {
-        // Make the HTTP request
-        HttpResponse response = new HttpRequestBuilder(HttpClients.createDefault())
-                .httpTransport(internalCluster().getDataNodeInstance(HttpServerTransport.class))
-                .addParam("error_trace", "true")
-                .method(HttpDeleteWithEntity.METHOD_NAME)
-                .execute();
-
-        assertThat(response.getHeaders().get("Content-Type"), is("application/json"));
-        assertThat(response.getBody(), is("{\"error\":\"error traces in responses are disabled.\"}"));
-        assertThat(response.getStatusCode(), is(400));
+        try {
+            getRestClient().performRequest("DELETE", "/", Collections.singletonMap("error_trace", "true"), null);
+            fail("request should have failed");
+        } catch(ResponseException e) {
+            Response response = e.getResponse();
+            assertThat(response.getHeader("Content-Type"), is("application/json"));
+            assertThat(e.getResponseBody(), is("{\"error\":\"error traces in responses are disabled.\"}"));
+            assertThat(response.getStatusLine().getStatusCode(), is(400));
+        }
     }
 }

+ 22 - 24
core/src/test/java/org/elasticsearch/options/detailederrors/DetailedErrorsEnabledIT.java

@@ -19,16 +19,15 @@
 
 package org.elasticsearch.options.detailederrors;
 
-import org.apache.http.impl.client.HttpClients;
+import org.elasticsearch.client.Response;
+import org.elasticsearch.client.ResponseException;
 import org.elasticsearch.common.network.NetworkModule;
 import org.elasticsearch.common.settings.Settings;
-import org.elasticsearch.http.HttpServerTransport;
 import org.elasticsearch.test.ESIntegTestCase;
 import org.elasticsearch.test.ESIntegTestCase.ClusterScope;
 import org.elasticsearch.test.ESIntegTestCase.Scope;
-import org.elasticsearch.test.rest.client.http.HttpDeleteWithEntity;
-import org.elasticsearch.test.rest.client.http.HttpRequestBuilder;
-import org.elasticsearch.test.rest.client.http.HttpResponse;
+
+import java.util.Collections;
 
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.not;
@@ -47,25 +46,24 @@ public class DetailedErrorsEnabledIT extends ESIntegTestCase {
     }
 
     public void testThatErrorTraceWorksByDefault() throws Exception {
-        // Make the HTTP request
-        HttpResponse response = new HttpRequestBuilder(HttpClients.createDefault())
-                .httpTransport(internalCluster().getDataNodeInstance(HttpServerTransport.class))
-                .path("/")
-                .addParam("error_trace", "true")
-                .method(HttpDeleteWithEntity.METHOD_NAME)
-                .execute();
-
-        assertThat(response.getHeaders().get("Content-Type"), containsString("application/json"));
-        assertThat(response.getBody(), containsString("\"stack_trace\":\"[Validation Failed: 1: index / indices is missing;]; nested: ActionRequestValidationException[Validation Failed: 1:"));
-
-        // Make the HTTP request
-        response = new HttpRequestBuilder(HttpClients.createDefault())
-                .httpTransport(internalCluster().getDataNodeInstance(HttpServerTransport.class))
-                .path("/")
-                .method(HttpDeleteWithEntity.METHOD_NAME)
-                .execute();
+        try {
+            getRestClient().performRequest("DELETE", "/", Collections.singletonMap("error_trace", "true"), null);
+            fail("request should have failed");
+        } catch(ResponseException e) {
+            Response response = e.getResponse();
+            assertThat(response.getHeader("Content-Type"), containsString("application/json"));
+            assertThat(e.getResponseBody(), containsString("\"stack_trace\":\"[Validation Failed: 1: index / indices is missing;]; " +
+                    "nested: ActionRequestValidationException[Validation Failed: 1:"));
+        }
 
-        assertThat(response.getHeaders().get("Content-Type"), containsString("application/json"));
-        assertThat(response.getBody(), not(containsString("\"stack_trace\":\"[Validation Failed: 1: index / indices is missing;]; nested: ActionRequestValidationException[Validation Failed: 1:")));
+        try {
+            getRestClient().performRequest("DELETE", "/", Collections.emptyMap(), null);
+            fail("request should have failed");
+        } catch(ResponseException e) {
+            Response response = e.getResponse();
+            assertThat(response.getHeader("Content-Type"), containsString("application/json"));
+            assertThat(e.getResponseBody(), not(containsString("\"stack_trace\":\"[Validation Failed: 1: index / indices is missing;]; "
+                    + "nested: ActionRequestValidationException[Validation Failed: 1:")));
+        }
     }
 }

+ 17 - 10
core/src/test/java/org/elasticsearch/plugins/ResponseHeaderPluginIT.java

@@ -18,18 +18,18 @@
  */
 package org.elasticsearch.plugins;
 
+import org.apache.http.message.BasicHeader;
+import org.elasticsearch.client.Response;
+import org.elasticsearch.client.ResponseException;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.plugins.responseheader.TestResponseHeaderPlugin;
 import org.elasticsearch.test.ESIntegTestCase;
 import org.elasticsearch.test.ESIntegTestCase.ClusterScope;
 import org.elasticsearch.test.ESIntegTestCase.Scope;
-import org.elasticsearch.test.rest.client.http.HttpResponse;
 
 import java.util.Collection;
+import java.util.Collections;
 
-import static org.elasticsearch.rest.RestStatus.OK;
-import static org.elasticsearch.rest.RestStatus.UNAUTHORIZED;
-import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.hasStatus;
 import static org.hamcrest.Matchers.equalTo;
 
 /**
@@ -52,12 +52,19 @@ public class ResponseHeaderPluginIT extends ESIntegTestCase {
 
     public void testThatSettingHeadersWorks() throws Exception {
         ensureGreen();
-        HttpResponse response = httpClient().method("GET").path("/_protected").execute();
-        assertThat(response, hasStatus(UNAUTHORIZED));
-        assertThat(response.getHeaders().get("Secret"), equalTo("required"));
+        try {
+            getRestClient().performRequest("GET", "/_protected", Collections.emptyMap(), null);
+            fail("request should have failed");
+        } catch(ResponseException e) {
+            Response response = e.getResponse();
+            assertThat(response.getStatusLine().getStatusCode(), equalTo(401));
+            assertThat(response.getHeader("Secret"), equalTo("required"));
+        }
 
-        HttpResponse authResponse = httpClient().method("GET").path("/_protected").addHeader("Secret", "password").execute();
-        assertThat(authResponse, hasStatus(OK));
-        assertThat(authResponse.getHeaders().get("Secret"), equalTo("granted"));
+        try (Response authResponse = getRestClient().performRequest("GET", "/_protected", Collections.emptyMap(), null,
+                new BasicHeader("Secret", "password"))) {
+            assertThat(authResponse.getStatusLine().getStatusCode(), equalTo(200));
+            assertThat(authResponse.getHeader("Secret"), equalTo("granted"));
+        }
     }
 }

+ 16 - 13
core/src/test/java/org/elasticsearch/rest/CorsNotSetIT.java

@@ -19,15 +19,17 @@
 
 package org.elasticsearch.rest;
 
+import org.apache.http.message.BasicHeader;
+import org.elasticsearch.client.Response;
 import org.elasticsearch.common.network.NetworkModule;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.test.ESIntegTestCase;
 import org.elasticsearch.test.ESIntegTestCase.ClusterScope;
-import org.elasticsearch.test.rest.client.http.HttpResponse;
 
-import static org.hamcrest.Matchers.hasKey;
+import java.util.Collections;
+
 import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.nullValue;
 
 /**
  *
@@ -44,18 +46,19 @@ public class CorsNotSetIT extends ESIntegTestCase {
 
     public void testCorsSettingDefaultBehaviourDoesNotReturnAnything() throws Exception {
         String corsValue = "http://localhost:9200";
-        HttpResponse response = httpClient().method("GET").path("/").addHeader("User-Agent", "Mozilla Bar").addHeader("Origin", corsValue).execute();
-
-        assertThat(response.getStatusCode(), is(200));
-        assertThat(response.getHeaders(), not(hasKey("Access-Control-Allow-Origin")));
-        assertThat(response.getHeaders(), not(hasKey("Access-Control-Allow-Credentials")));
+        try (Response response = getRestClient().performRequest("GET", "/", Collections.emptyMap(), null,
+                new BasicHeader("User-Agent", "Mozilla Bar"), new BasicHeader("Origin", corsValue))) {
+            assertThat(response.getStatusLine().getStatusCode(), is(200));
+            assertThat(response.getHeader("Access-Control-Allow-Origin"), nullValue());
+            assertThat(response.getHeader("Access-Control-Allow-Credentials"), nullValue());
+        }
     }
 
     public void testThatOmittingCorsHeaderDoesNotReturnAnything() throws Exception {
-        HttpResponse response = httpClient().method("GET").path("/").execute();
-
-        assertThat(response.getStatusCode(), is(200));
-        assertThat(response.getHeaders(), not(hasKey("Access-Control-Allow-Origin")));
-        assertThat(response.getHeaders(), not(hasKey("Access-Control-Allow-Credentials")));
+        try (Response response = getRestClient().performRequest("GET", "/", Collections.emptyMap(), null)) {
+            assertThat(response.getStatusLine().getStatusCode(), is(200));
+            assertThat(response.getHeader("Access-Control-Allow-Origin"), nullValue());
+            assertThat(response.getHeader("Access-Control-Allow-Credentials"), nullValue());
+        }
     }
 }

+ 55 - 43
core/src/test/java/org/elasticsearch/rest/CorsRegexIT.java

@@ -18,6 +18,9 @@
  */
 package org.elasticsearch.rest;
 
+import org.apache.http.message.BasicHeader;
+import org.elasticsearch.client.Response;
+import org.elasticsearch.client.ResponseException;
 import org.elasticsearch.common.logging.ESLogger;
 import org.elasticsearch.common.logging.Loggers;
 import org.elasticsearch.common.network.NetworkModule;
@@ -25,16 +28,16 @@ import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.test.ESIntegTestCase;
 import org.elasticsearch.test.ESIntegTestCase.ClusterScope;
 import org.elasticsearch.test.ESIntegTestCase.Scope;
-import org.elasticsearch.test.rest.client.http.HttpResponse;
 import org.jboss.netty.handler.codec.http.HttpHeaders;
 
+import java.util.Collections;
+
 import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_CREDENTIALS;
 import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_METHODS;
 import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_ORIGIN;
 import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ENABLED;
-import static org.hamcrest.Matchers.hasKey;
 import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.nullValue;
 
 /**
  * Test CORS where the allow origin value is a regular expression.
@@ -58,64 +61,73 @@ public class CorsRegexIT extends ESIntegTestCase {
 
     public void testThatRegularExpressionWorksOnMatch() throws Exception {
         String corsValue = "http://localhost:9200";
-        HttpResponse response = httpClient().method("GET").path("/").addHeader("User-Agent", "Mozilla Bar").addHeader("Origin", corsValue).execute();
-        assertResponseWithOriginheader(response, corsValue);
-
+        try (Response response = getRestClient().performRequest("GET", "/", Collections.emptyMap(), null,
+                new BasicHeader("User-Agent", "Mozilla Bar"), new BasicHeader("Origin", corsValue))) {
+            assertResponseWithOriginheader(response, corsValue);
+        }
         corsValue = "https://localhost:9200";
-        response = httpClient().method("GET").path("/").addHeader("User-Agent", "Mozilla Bar").addHeader("Origin", corsValue).execute();
-        assertResponseWithOriginheader(response, corsValue);
-        assertThat(response.getHeaders(), hasKey("Access-Control-Allow-Credentials"));
-        assertThat(response.getHeaders().get("Access-Control-Allow-Credentials"), is("true"));
+        try (Response response = getRestClient().performRequest("GET", "/", Collections.emptyMap(), null,
+                new BasicHeader("User-Agent", "Mozilla Bar"), new BasicHeader("Origin", corsValue));) {
+            assertResponseWithOriginheader(response, corsValue);
+            assertThat(response.getHeader("Access-Control-Allow-Credentials"), is("true"));
+        }
     }
 
     public void testThatRegularExpressionReturnsForbiddenOnNonMatch() throws Exception {
-        HttpResponse response = httpClient().method("GET").path("/").addHeader("User-Agent", "Mozilla Bar").addHeader("Origin", "http://evil-host:9200").execute();
-        // a rejected origin gets a FORBIDDEN - 403
-        assertThat(response.getStatusCode(), is(403));
-        assertThat(response.getHeaders(), not(hasKey("Access-Control-Allow-Origin")));
+        try {
+            getRestClient().performRequest("GET", "/", Collections.emptyMap(), null, new BasicHeader("User-Agent", "Mozilla Bar"),
+                    new BasicHeader("Origin", "http://evil-host:9200"));
+            fail("request should have failed");
+        } catch(ResponseException e) {
+            Response response = e.getResponse();
+            // a rejected origin gets a FORBIDDEN - 403
+            assertThat(response.getStatusLine().getStatusCode(), is(403));
+            assertThat(response.getHeader("Access-Control-Allow-Origin"), nullValue());
+        }
     }
 
     public void testThatSendingNoOriginHeaderReturnsNoAccessControlHeader() throws Exception {
-        HttpResponse response = httpClient().method("GET").path("/").addHeader("User-Agent", "Mozilla Bar").execute();
-        assertThat(response.getStatusCode(), is(200));
-        assertThat(response.getHeaders(), not(hasKey("Access-Control-Allow-Origin")));
+        try (Response response = getRestClient().performRequest("GET", "/", Collections.emptyMap(), null,
+                new BasicHeader("User-Agent", "Mozilla Bar"))) {
+            assertThat(response.getStatusLine().getStatusCode(), is(200));
+            assertThat(response.getHeader("Access-Control-Allow-Origin"), nullValue());
+        }
     }
 
     public void testThatRegularExpressionIsNotAppliedWithoutCorrectBrowserOnMatch() throws Exception {
-        HttpResponse response = httpClient().method("GET").path("/").execute();
-        assertThat(response.getStatusCode(), is(200));
-        assertThat(response.getHeaders(), not(hasKey("Access-Control-Allow-Origin")));
+        try (Response response = getRestClient().performRequest("GET", "/", Collections.emptyMap(), null)) {
+            assertThat(response.getStatusLine().getStatusCode(), is(200));
+            assertThat(response.getHeader("Access-Control-Allow-Origin"), nullValue());
+        }
     }
 
     public void testThatPreFlightRequestWorksOnMatch() throws Exception {
         String corsValue = "http://localhost:9200";
-        HttpResponse response = httpClient().method("OPTIONS")
-                                    .path("/")
-                                    .addHeader("User-Agent", "Mozilla Bar")
-                                    .addHeader("Origin", corsValue)
-                                    .addHeader(HttpHeaders.Names.ACCESS_CONTROL_REQUEST_METHOD, "GET")
-                                    .execute();
-        assertResponseWithOriginheader(response, corsValue);
-        assertThat(response.getHeaders(), hasKey("Access-Control-Allow-Methods"));
+        try (Response response = getRestClient().performRequest("OPTIONS", "/", Collections.emptyMap(), null,
+                new BasicHeader("User-Agent", "Mozilla Bar"), new BasicHeader("Origin", corsValue),
+                new BasicHeader(HttpHeaders.Names.ACCESS_CONTROL_REQUEST_METHOD, "GET"));) {
+            assertResponseWithOriginheader(response, corsValue);
+            assertNotNull(response.getHeader("Access-Control-Allow-Methods"));
+        }
     }
 
     public void testThatPreFlightRequestReturnsNullOnNonMatch() throws Exception {
-        HttpResponse response = httpClient().method("OPTIONS")
-                                    .path("/")
-                                    .addHeader("User-Agent", "Mozilla Bar")
-                                    .addHeader("Origin", "http://evil-host:9200")
-                                    .addHeader(HttpHeaders.Names.ACCESS_CONTROL_REQUEST_METHOD, "GET")
-                                    .execute();
-        // a rejected origin gets a FORBIDDEN - 403
-        assertThat(response.getStatusCode(), is(403));
-        assertThat(response.getHeaders(), not(hasKey("Access-Control-Allow-Origin")));
-        assertThat(response.getHeaders(), not(hasKey("Access-Control-Allow-Methods")));
+        try {
+            getRestClient().performRequest("OPTIONS", "/", Collections.emptyMap(), null, new BasicHeader("User-Agent", "Mozilla Bar"),
+                    new BasicHeader("Origin", "http://evil-host:9200"),
+                    new BasicHeader(HttpHeaders.Names.ACCESS_CONTROL_REQUEST_METHOD, "GET"));
+            fail("request should have failed");
+        } catch(ResponseException e) {
+            Response response = e.getResponse();
+            // a rejected origin gets a FORBIDDEN - 403
+            assertThat(response.getStatusLine().getStatusCode(), is(403));
+            assertThat(response.getHeader("Access-Control-Allow-Origin"), nullValue());
+            assertThat(response.getHeader("Access-Control-Allow-Methods"), nullValue());
+        }
     }
 
-    protected static void assertResponseWithOriginheader(HttpResponse response, String expectedCorsHeader) {
-        assertThat(response.getStatusCode(), is(200));
-        assertThat(response.getHeaders(), hasKey("Access-Control-Allow-Origin"));
-        assertThat(response.getHeaders().get("Access-Control-Allow-Origin"), is(expectedCorsHeader));
+    protected static void assertResponseWithOriginheader(Response response, String expectedCorsHeader) {
+        assertThat(response.getStatusLine().getStatusCode(), is(200));
+        assertThat(response.getHeader("Access-Control-Allow-Origin"), is(expectedCorsHeader));
     }
-
 }

+ 12 - 8
core/src/test/java/org/elasticsearch/rest/action/main/RestMainActionIT.java

@@ -18,16 +18,17 @@
  */
 package org.elasticsearch.rest.action.main;
 
+import org.apache.http.util.EntityUtils;
+import org.elasticsearch.client.Response;
 import org.elasticsearch.common.network.NetworkModule;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.test.ESIntegTestCase;
-import org.elasticsearch.test.rest.client.http.HttpResponse;
 
 import java.io.IOException;
+import java.util.Collections;
 
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
-import static org.hamcrest.Matchers.nullValue;
 
 public class RestMainActionIT extends ESIntegTestCase {
     @Override
@@ -39,14 +40,17 @@ public class RestMainActionIT extends ESIntegTestCase {
     }
 
     public void testHeadRequest() throws IOException {
-        final HttpResponse response = httpClient().method("HEAD").path("/").execute();
-        assertThat(response.getStatusCode(), equalTo(200));
-        assertThat(response.getBody(), nullValue());
+        try (Response response = getRestClient().performRequest("HEAD", "/", Collections.emptyMap(), null)) {
+            assertThat(response.getStatusLine().getStatusCode(), equalTo(200));
+            assertNull(response.getEntity());
+        }
     }
 
     public void testGetRequest() throws IOException {
-        final HttpResponse response = httpClient().path("/").execute();
-        assertThat(response.getStatusCode(), equalTo(200));
-        assertThat(response.getBody(), containsString("cluster_name"));
+        try (Response response = getRestClient().performRequest("GET", "/", Collections.emptyMap(), null)) {
+            assertThat(response.getStatusLine().getStatusCode(), equalTo(200));
+            assertNotNull(response.getEntity());
+            assertThat(EntityUtils.toString(response.getEntity()), containsString("cluster_name"));
+        }
     }
 }

+ 17 - 25
core/src/test/java/org/elasticsearch/transport/ContextAndHeaderTransportIT.java

@@ -19,8 +19,7 @@
 
 package org.elasticsearch.transport;
 
-import org.apache.http.impl.client.CloseableHttpClient;
-import org.apache.http.impl.client.HttpClients;
+import org.apache.http.message.BasicHeader;
 import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.ActionModule;
 import org.elasticsearch.action.ActionRequest;
@@ -33,12 +32,12 @@ import org.elasticsearch.action.search.SearchResponse;
 import org.elasticsearch.action.support.ActionFilter;
 import org.elasticsearch.action.termvectors.MultiTermVectorsRequest;
 import org.elasticsearch.client.Client;
+import org.elasticsearch.client.Response;
 import org.elasticsearch.common.inject.AbstractModule;
 import org.elasticsearch.common.inject.Inject;
 import org.elasticsearch.common.inject.Module;
 import org.elasticsearch.common.network.NetworkModule;
 import org.elasticsearch.common.settings.Settings;
-import org.elasticsearch.http.HttpServerTransport;
 import org.elasticsearch.index.query.BoolQueryBuilder;
 import org.elasticsearch.index.query.GeoShapeQueryBuilder;
 import org.elasticsearch.index.query.MoreLikeThisQueryBuilder;
@@ -50,8 +49,6 @@ import org.elasticsearch.plugins.Plugin;
 import org.elasticsearch.rest.RestController;
 import org.elasticsearch.test.ESIntegTestCase;
 import org.elasticsearch.test.ESIntegTestCase.ClusterScope;
-import org.elasticsearch.test.rest.client.http.HttpRequestBuilder;
-import org.elasticsearch.test.rest.client.http.HttpResponse;
 import org.elasticsearch.threadpool.ThreadPool;
 import org.junit.After;
 import org.junit.Before;
@@ -66,12 +63,11 @@ import java.util.concurrent.CopyOnWriteArrayList;
 
 import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF_SHARDS;
 import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
-import static org.elasticsearch.rest.RestStatus.OK;
 import static org.elasticsearch.test.ESIntegTestCase.Scope.SUITE;
 import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
 import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount;
 import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoFailures;
-import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.hasStatus;
+import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.greaterThan;
 import static org.hamcrest.Matchers.hasSize;
 import static org.hamcrest.Matchers.is;
@@ -217,26 +213,22 @@ public class ContextAndHeaderTransportIT extends ESIntegTestCase {
     }
 
     public void testThatRelevantHttpHeadersBecomeRequestHeaders() throws Exception {
-        String releventHeaderName = "relevant_" + randomHeaderKey;
-        for (RestController restController : internalCluster().getDataNodeInstances(RestController.class)) {
-            restController.registerRelevantHeaders(releventHeaderName);
+        String relevantHeaderName = "relevant_" + randomHeaderKey;
+        for (RestController restController : internalCluster().getInstances(RestController.class)) {
+            restController.registerRelevantHeaders(relevantHeaderName);
         }
 
-        CloseableHttpClient httpClient = HttpClients.createDefault();
-        HttpResponse response = new HttpRequestBuilder(httpClient)
-            .httpTransport(internalCluster().getDataNodeInstance(HttpServerTransport.class))
-            .addHeader(randomHeaderKey, randomHeaderValue)
-            .addHeader(releventHeaderName, randomHeaderValue)
-            .path("/" + queryIndex + "/_search")
-            .execute();
-
-        assertThat(response, hasStatus(OK));
-        List<RequestAndHeaders> searchRequests = getRequests(SearchRequest.class);
-        assertThat(searchRequests, hasSize(greaterThan(0)));
-        for (RequestAndHeaders requestAndHeaders : searchRequests) {
-            assertThat(requestAndHeaders.headers.containsKey(releventHeaderName), is(true));
-            // was not specified, thus is not included
-            assertThat(requestAndHeaders.headers.containsKey(randomHeaderKey), is(false));
+        try (Response response = getRestClient().performRequest(
+                "GET", "/" + queryIndex + "/_search", Collections.emptyMap(), null,
+                new BasicHeader(randomHeaderKey, randomHeaderValue), new BasicHeader(relevantHeaderName, randomHeaderValue))) {
+            assertThat(response.getStatusLine().getStatusCode(), equalTo(200));
+            List<RequestAndHeaders> searchRequests = getRequests(SearchRequest.class);
+            assertThat(searchRequests, hasSize(greaterThan(0)));
+            for (RequestAndHeaders requestAndHeaders : searchRequests) {
+                assertThat(requestAndHeaders.headers.containsKey(relevantHeaderName), is(true));
+                // was not specified, thus is not included
+                assertThat(requestAndHeaders.headers.containsKey(randomHeaderKey), is(false));
+            }
         }
     }
 

+ 2 - 0
settings.gradle

@@ -5,6 +5,8 @@ List projects = [
   'rest-api-spec',
   'core',
   'docs',
+  'client',
+  'client-sniffer',
   'benchmarks',
   'distribution:integ-test-zip',
   'distribution:zip',

+ 3 - 2
test/framework/build.gradle

@@ -23,14 +23,15 @@ dependencies {
   compile "org.elasticsearch:elasticsearch:${version}"
   compile "com.carrotsearch.randomizedtesting:randomizedtesting-runner:${versions.randomizedrunner}"
   compile "junit:junit:${versions.junit}"
-  compile 'org.hamcrest:hamcrest-all:1.3'
+  compile "org.hamcrest:hamcrest-all:${versions.hamcrest}"
   compile "org.apache.lucene:lucene-test-framework:${versions.lucene}"
   compile "org.apache.lucene:lucene-codecs:${versions.lucene}"
+  compile "org.elasticsearch:client:${version}"
   compile "org.apache.httpcomponents:httpclient:${versions.httpclient}"
   compile "org.apache.httpcomponents:httpcore:${versions.httpcore}"
   compile "commons-logging:commons-logging:${versions.commonslogging}"
   compile "commons-codec:commons-codec:${versions.commonscodec}"
-  compile 'org.elasticsearch:securemock:1.2'
+  compile "org.elasticsearch:securemock:${versions.securemock}"
 }
 
 compileJava.options.compilerArgs << '-Xlint:-cast,-rawtypes,-try,-unchecked'

+ 37 - 10
test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java

@@ -22,8 +22,8 @@ import com.carrotsearch.randomizedtesting.RandomizedContext;
 import com.carrotsearch.randomizedtesting.annotations.TestGroup;
 import com.carrotsearch.randomizedtesting.generators.RandomInts;
 import com.carrotsearch.randomizedtesting.generators.RandomPicks;
+import org.apache.http.HttpHost;
 import org.apache.http.impl.client.CloseableHttpClient;
-import org.apache.http.impl.client.HttpClients;
 import org.apache.lucene.util.IOUtils;
 import org.apache.lucene.util.LuceneTestCase;
 import org.apache.lucene.util.TestUtil;
@@ -57,6 +57,7 @@ import org.elasticsearch.action.support.IndicesOptions;
 import org.elasticsearch.client.AdminClient;
 import org.elasticsearch.client.Client;
 import org.elasticsearch.client.Requests;
+import org.elasticsearch.client.RestClient;
 import org.elasticsearch.cluster.ClusterState;
 import org.elasticsearch.cluster.health.ClusterHealthStatus;
 import org.elasticsearch.cluster.metadata.IndexMetaData;
@@ -118,7 +119,6 @@ import org.elasticsearch.search.MockSearchService;
 import org.elasticsearch.search.SearchHit;
 import org.elasticsearch.test.client.RandomizingClient;
 import org.elasticsearch.test.disruption.ServiceDisruptionScheme;
-import org.elasticsearch.test.rest.client.http.HttpRequestBuilder;
 import org.elasticsearch.test.store.MockFSIndexStore;
 import org.elasticsearch.test.transport.AssertingLocalTransport;
 import org.elasticsearch.test.transport.MockTransportService;
@@ -314,6 +314,7 @@ public abstract class ESIntegTestCase extends ESTestCase {
      * By default if no {@link ClusterScope} is configured this will hold a reference to the suite cluster.
      */
     private static TestCluster currentCluster;
+    private static RestClient restClient = null;
 
     private static final double TRANSPORT_CLIENT_RATIO = transportClientRatio();
 
@@ -504,6 +505,10 @@ public abstract class ESIntegTestCase extends ESTestCase {
             IOUtils.close(clusters.values());
             clusters.clear();
         }
+        if (restClient != null) {
+            restClient.close();
+            restClient = null;
+        }
     }
 
     protected final void afterInternal(boolean afterClass) throws Exception {
@@ -2024,21 +2029,43 @@ public abstract class ESIntegTestCase extends ESTestCase {
         return builder.build();
     }
 
-    protected HttpRequestBuilder httpClient() {
-        return httpClient(HttpClients.createDefault());
+    /**
+     * Returns an instance of {@link RestClient} pointing to the current test cluster.
+     * Creates a new client if the method is invoked for the first time in the context of the current test scope.
+     * The returned client gets automatically closed when needed, it shouldn't be closed as part of tests otherwise
+     * it cannot be reused by other tests anymore.
+     */
+    protected synchronized static RestClient getRestClient() {
+        if (restClient == null) {
+            restClient = createRestClient(null);
+        }
+        return restClient;
+    }
+
+    protected static RestClient createRestClient(CloseableHttpClient httpClient) {
+        return createRestClient(httpClient, "http");
     }
 
-    protected HttpRequestBuilder httpClient(CloseableHttpClient httpClient) {
+    protected static RestClient createRestClient(CloseableHttpClient httpClient, String protocol) {
         final NodesInfoResponse nodeInfos = client().admin().cluster().prepareNodesInfo().get();
         final List<NodeInfo> nodes = nodeInfos.getNodes();
         assertFalse(nodeInfos.hasFailures());
-        TransportAddress publishAddress = randomFrom(nodes).getHttp().address().publishAddress();
-        assertEquals(1, publishAddress.uniqueAddressTypeId());
-        InetSocketAddress address = ((InetSocketTransportAddress) publishAddress).address();
-        return new HttpRequestBuilder(httpClient).host(NetworkAddress.format(address.getAddress())).port(address.getPort());
+        List<HttpHost> hosts = new ArrayList<>();
+        for (NodeInfo node : nodes) {
+            if (node.getHttp() != null) {
+                TransportAddress publishAddress = node.getHttp().address().publishAddress();
+                assertEquals(1, publishAddress.uniqueAddressTypeId());
+                InetSocketAddress address = ((InetSocketTransportAddress) publishAddress).address();
+                hosts.add(new HttpHost(NetworkAddress.format(address.getAddress()), address.getPort(), protocol));
+            }
+        }
+        RestClient.Builder builder = RestClient.builder(hosts.toArray(new HttpHost[hosts.size()]));
+        if (httpClient != null) {
+            builder.setHttpClient(httpClient);
+        }
+        return builder.build();
     }
 
-
     /**
      * This method is executed iff the test is annotated with {@link SuiteScopeTestCase}
      * before the first test of this class is executed.

+ 0 - 7
test/framework/src/main/java/org/elasticsearch/test/hamcrest/ElasticsearchAssertions.java

@@ -60,7 +60,6 @@ import org.elasticsearch.search.SearchModule;
 import org.elasticsearch.search.suggest.Suggest;
 import org.elasticsearch.test.ESIntegTestCase;
 import org.elasticsearch.test.VersionUtils;
-import org.elasticsearch.test.rest.client.http.HttpResponse;
 import org.hamcrest.CoreMatchers;
 import org.hamcrest.Matcher;
 import org.hamcrest.Matchers;
@@ -82,7 +81,6 @@ import static org.elasticsearch.test.VersionUtils.randomVersion;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.contains;
 import static org.hamcrest.Matchers.greaterThan;
 import static org.hamcrest.Matchers.greaterThanOrEqualTo;
 import static org.hamcrest.Matchers.hasItem;
@@ -90,7 +88,6 @@ import static org.hamcrest.Matchers.hasKey;
 import static org.hamcrest.Matchers.instanceOf;
 import static org.hamcrest.Matchers.not;
 import static org.hamcrest.Matchers.notNullValue;
-import static org.hamcrest.Matchers.nullValue;
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -488,10 +485,6 @@ public class ElasticsearchAssertions {
         return new ElasticsearchMatchers.SearchHitHasScoreMatcher(score);
     }
 
-    public static Matcher<HttpResponse> hasStatus(RestStatus restStatus) {
-        return new ElasticsearchMatchers.HttpResponseHasStatusMatcher(restStatus);
-    }
-
     public static <T extends Query> T assertBooleanSubQuery(Query query, Class<T> subqueryType, int i) {
         assertThat(query, instanceOf(BooleanQuery.class));
         BooleanQuery q = (BooleanQuery) query;

+ 0 - 26
test/framework/src/main/java/org/elasticsearch/test/hamcrest/ElasticsearchMatchers.java

@@ -18,9 +18,7 @@
  */
 package org.elasticsearch.test.hamcrest;
 
-import org.elasticsearch.rest.RestStatus;
 import org.elasticsearch.search.SearchHit;
-import org.elasticsearch.test.rest.client.http.HttpResponse;
 import org.hamcrest.Description;
 import org.hamcrest.TypeSafeMatcher;
 
@@ -117,28 +115,4 @@ public class ElasticsearchMatchers {
             description.appendText("searchHit score should be ").appendValue(score);
         }
     }
-
-    public static class HttpResponseHasStatusMatcher extends TypeSafeMatcher<HttpResponse> {
-
-        private RestStatus restStatus;
-
-        public HttpResponseHasStatusMatcher(RestStatus restStatus) {
-            this.restStatus = restStatus;
-        }
-
-        @Override
-        protected boolean matchesSafely(HttpResponse response) {
-            return response.getStatusCode() == restStatus.getStatus();
-        }
-
-        @Override
-        public void describeMismatchSafely(final HttpResponse response, final Description mismatchDescription) {
-            mismatchDescription.appendText(" was ").appendValue(response.getStatusCode());
-        }
-
-        @Override
-        public void describeTo(final Description description) {
-            description.appendText("HTTP response status code should be ").appendValue(restStatus.getStatus());
-        }
-    }
 }

+ 34 - 38
test/framework/src/main/java/org/elasticsearch/test/rest/ESRestTestCase.java

@@ -19,34 +19,16 @@
 
 package org.elasticsearch.test.rest;
 
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.nio.file.FileSystem;
-import java.nio.file.FileSystems;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.StandardCopyOption;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
+import com.carrotsearch.randomizedtesting.RandomizedTest;
 import org.apache.lucene.util.IOUtils;
 import org.elasticsearch.action.admin.cluster.node.tasks.list.ListTasksAction;
+import org.elasticsearch.client.ResponseException;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.SuppressForbidden;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.xcontent.XContentHelper;
 import org.elasticsearch.test.ESTestCase;
-import org.elasticsearch.test.rest.client.RestException;
-import org.elasticsearch.test.rest.client.RestResponse;
+import org.elasticsearch.test.rest.client.RestTestResponse;
 import org.elasticsearch.test.rest.parser.RestTestParseException;
 import org.elasticsearch.test.rest.parser.RestTestSuiteParser;
 import org.elasticsearch.test.rest.section.DoSection;
@@ -62,7 +44,24 @@ import org.junit.AfterClass;
 import org.junit.Before;
 import org.junit.BeforeClass;
 
-import com.carrotsearch.randomizedtesting.RandomizedTest;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 import static java.util.Collections.emptyList;
 import static java.util.Collections.emptyMap;
@@ -98,8 +97,8 @@ public abstract class ESRestTestCase extends ESTestCase {
     private static final String DEFAULT_SPEC_PATH = "/rest-api-spec/api";
 
     /**
-     * This separator pattern matches ',' except it is preceded by a '\'. This allows us to support ',' within paths when it is escaped with
-     * a slash.
+     * This separator pattern matches ',' except it is preceded by a '\'.
+     * This allows us to support ',' within paths when it is escaped with a slash.
      *
      * For example, the path string "/a/b/c\,d/e/f,/foo/bar,/baz" is separated to "/a/b/c\,d/e/f", "/foo/bar" and "/baz".
      *
@@ -233,7 +232,7 @@ public abstract class ESRestTestCase extends ESTestCase {
     }
 
     @BeforeClass
-    public static void initExecutionContext() throws IOException, RestException {
+    public static void initExecutionContext() throws IOException {
         String[] specPaths = resolvePathsProperty(REST_TESTS_SPEC, DEFAULT_SPEC_PATH);
         RestSpec restSpec = null;
         FileSystem fileSystem = getFileSystem();
@@ -277,9 +276,9 @@ public abstract class ESRestTestCase extends ESTestCase {
         deleteIndicesArgs.put("index", "*");
         try {
             adminExecutionContext.callApi("indices.delete", deleteIndicesArgs, Collections.emptyList(), Collections.emptyMap());
-        } catch (RestException e) {
+        } catch (ResponseException e) {
             // 404 here just means we had no indexes
-            if (e.statusCode() != 404) {
+            if (e.getResponse().getStatusLine().getStatusCode() != 404) {
                 throw e;
             }
         }
@@ -300,8 +299,8 @@ public abstract class ESRestTestCase extends ESTestCase {
      * other tests.
      */
     @After
-    public void logIfThereAreRunningTasks() throws InterruptedException, IOException, RestException {
-        RestResponse tasks = adminExecutionContext.callApi("tasks.list", emptyMap(), emptyList(), emptyMap());
+    public void logIfThereAreRunningTasks() throws InterruptedException, IOException {
+        RestTestResponse tasks = adminExecutionContext.callApi("tasks.list", emptyMap(), emptyList(), emptyMap());
         Set<String> runningTasks = runningTasks(tasks);
         // Ignore the task list API - it doens't count against us
         runningTasks.remove(ListTasksAction.NAME);
@@ -341,13 +340,8 @@ public abstract class ESRestTestCase extends ESTestCase {
         return restClientSettings(); // default to the same client settings
     }
 
-    /** Returns the addresses the client uses to connect to the test cluster. */
-    protected URL[] getClusterUrls() {
-        return clusterUrls;
-    }
-
     @Before
-    public void reset() throws IOException, RestException {
+    public void reset() throws IOException {
         // admin context must be available for @After always, regardless of whether the test was blacklisted
         adminExecutionContext.initClient(clusterUrls, restAdminSettings());
         adminExecutionContext.clear();
@@ -355,7 +349,8 @@ public abstract class ESRestTestCase extends ESTestCase {
         //skip test if it matches one of the blacklist globs
         for (BlacklistedPathPatternMatcher blacklistedPathMatcher : blacklistPathMatchers) {
             String testPath = testCandidate.getSuitePath() + "/" + testCandidate.getTestSection().getName();
-            assumeFalse("[" + testCandidate.getTestPath() + "] skipped, reason: blacklisted", blacklistedPathMatcher.isSuffixMatch(testPath));
+            assumeFalse("[" + testCandidate.getTestPath() + "] skipped, reason: blacklisted", blacklistedPathMatcher
+                    .isSuffixMatch(testPath));
         }
         //The client needs non static info to get initialized, therefore it can't be initialized in the before class
         restTestExecutionContext.initClient(clusterUrls, restClientSettings());
@@ -374,7 +369,8 @@ public abstract class ESRestTestCase extends ESTestCase {
         if (skipSection.isVersionCheck()) {
             messageBuilder.append("[").append(description).append("] skipped, reason: [").append(skipSection.getReason()).append("] ");
         } else {
-            messageBuilder.append("[").append(description).append("] skipped, reason: features ").append(skipSection.getFeatures()).append(" not supported");
+            messageBuilder.append("[").append(description).append("] skipped, reason: features ")
+                    .append(skipSection.getFeatures()).append(" not supported");
         }
         return messageBuilder.toString();
     }
@@ -401,7 +397,7 @@ public abstract class ESRestTestCase extends ESTestCase {
     }
 
     @SuppressWarnings("unchecked")
-    public Set<String> runningTasks(RestResponse response) throws IOException {
+    public Set<String> runningTasks(RestTestResponse response) throws IOException {
         Set<String> runningTasks = new HashSet<>();
         Map<String, Object> nodes = (Map<String, Object>) response.evaluate("nodes");
         for (Map.Entry<String, Object> node : nodes.entrySet()) {

+ 18 - 19
test/framework/src/main/java/org/elasticsearch/test/rest/RestTestExecutionContext.java

@@ -19,18 +19,17 @@
 package org.elasticsearch.test.rest;
 
 import org.elasticsearch.Version;
+import org.elasticsearch.client.ResponseException;
 import org.elasticsearch.common.logging.ESLogger;
 import org.elasticsearch.common.logging.Loggers;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.xcontent.XContentFactory;
-import org.elasticsearch.test.rest.client.RestClient;
-import org.elasticsearch.test.rest.client.RestException;
-import org.elasticsearch.test.rest.client.RestResponse;
+import org.elasticsearch.test.rest.client.RestTestClient;
+import org.elasticsearch.test.rest.client.RestTestResponse;
 import org.elasticsearch.test.rest.spec.RestSpec;
 
 import java.io.Closeable;
 import java.io.IOException;
-import java.net.InetSocketAddress;
 import java.net.URL;
 import java.util.HashMap;
 import java.util.List;
@@ -50,9 +49,9 @@ public class RestTestExecutionContext implements Closeable {
 
     private final RestSpec restSpec;
 
-    private RestClient restClient;
+    private RestTestClient restTestClient;
 
-    private RestResponse response;
+    private RestTestResponse response;
 
     public RestTestExecutionContext(RestSpec restSpec) {
         this.restSpec = restSpec;
@@ -61,10 +60,9 @@ public class RestTestExecutionContext implements Closeable {
     /**
      * Calls an elasticsearch api with the parameters and request body provided as arguments.
      * Saves the obtained response in the execution context.
-     * @throws RestException if the returned status code is non ok
      */
-    public RestResponse callApi(String apiName, Map<String, String> params, List<Map<String, Object>> bodies,
-                                Map<String, String> headers) throws IOException, RestException  {
+    public RestTestResponse callApi(String apiName, Map<String, String> params, List<Map<String, Object>> bodies,
+                                    Map<String, String> headers) throws IOException  {
         //makes a copy of the parameters before modifying them for this specific request
         HashMap<String, String> requestParams = new HashMap<>(params);
         for (Map.Entry<String, String> entry : requestParams.entrySet()) {
@@ -80,8 +78,8 @@ public class RestTestExecutionContext implements Closeable {
             //we always stash the last response body
             stash.stashResponse(response);
             return response;
-        } catch(RestException e) {
-            response = e.restResponse();
+        } catch(ResponseException e) {
+            response = new RestTestResponse(e);
             throw e;
         }
     }
@@ -106,8 +104,9 @@ public class RestTestExecutionContext implements Closeable {
         return XContentFactory.jsonBuilder().map(body).string();
     }
 
-    private RestResponse callApiInternal(String apiName, Map<String, String> params, String body, Map<String, String> headers) throws IOException, RestException  {
-        return restClient.callApi(apiName, params, body, headers);
+    private RestTestResponse callApiInternal(String apiName, Map<String, String> params, String body, Map<String, String> headers)
+            throws IOException  {
+        return restTestClient.callApi(apiName, params, body, headers);
     }
 
     /**
@@ -120,9 +119,9 @@ public class RestTestExecutionContext implements Closeable {
     /**
      * Creates the embedded REST client when needed. Needs to be called before each test.
      */
-    public void initClient(URL[] urls, Settings settings) throws IOException, RestException {
-        if (restClient == null) {
-            restClient = new RestClient(restSpec, settings, urls);
+    public void initClient(URL[] urls, Settings settings) throws IOException {
+        if (restTestClient == null) {
+            restTestClient = new RestTestClient(restSpec, settings, urls);
         }
     }
 
@@ -143,7 +142,7 @@ public class RestTestExecutionContext implements Closeable {
      * Returns the current es version as a string
      */
     public Version esVersion() {
-        return restClient.getEsVersion();
+        return restTestClient.getEsVersion();
     }
 
     /**
@@ -151,8 +150,8 @@ public class RestTestExecutionContext implements Closeable {
      */
     @Override
     public void close() {
-        if (restClient != null) {
-            restClient.close();
+        if (restTestClient != null) {
+            restTestClient.close();
         }
     }
 }

+ 8 - 8
test/framework/src/main/java/org/elasticsearch/test/rest/Stash.java

@@ -19,17 +19,17 @@
 
 package org.elasticsearch.test.rest;
 
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.logging.ESLogger;
 import org.elasticsearch.common.logging.Loggers;
 import org.elasticsearch.common.xcontent.ToXContent;
 import org.elasticsearch.common.xcontent.XContentBuilder;
-import org.elasticsearch.test.rest.client.RestResponse;
+import org.elasticsearch.test.rest.client.RestTestResponse;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 /**
  * Allows to cache the last obtained test response and or part of it within variables
@@ -42,7 +42,7 @@ public class Stash implements ToXContent {
     public static final Stash EMPTY = new Stash();
 
     private final Map<String, Object> stash = new HashMap<>();
-    private RestResponse response;
+    private RestTestResponse response;
 
     /**
      * Allows to saved a specific field in the stash as key-value pair
@@ -55,7 +55,7 @@ public class Stash implements ToXContent {
         }
     }
 
-    public void stashResponse(RestResponse response) throws IOException {
+    public void stashResponse(RestTestResponse response) throws IOException {
         // TODO we can almost certainly save time by lazily evaluating the body
         stashValue("body", response.getBody());
         this.response = response;

+ 137 - 131
test/framework/src/main/java/org/elasticsearch/test/rest/client/RestClient.java → test/framework/src/main/java/org/elasticsearch/test/rest/client/RestTestClient.java

@@ -19,17 +19,23 @@
 package org.elasticsearch.test.rest.client;
 
 import com.carrotsearch.randomizedtesting.RandomizedTest;
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpHost;
 import org.apache.http.config.Registry;
 import org.apache.http.config.RegistryBuilder;
 import org.apache.http.conn.socket.ConnectionSocketFactory;
 import org.apache.http.conn.socket.PlainConnectionSocketFactory;
 import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.entity.StringEntity;
 import org.apache.http.impl.client.CloseableHttpClient;
-import org.apache.http.impl.client.HttpClients;
-import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.apache.http.message.BasicHeader;
 import org.apache.http.ssl.SSLContexts;
 import org.apache.lucene.util.IOUtils;
 import org.elasticsearch.Version;
+import org.elasticsearch.client.Response;
+import org.elasticsearch.client.ResponseException;
+import org.elasticsearch.client.RestClient;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.io.PathUtils;
 import org.elasticsearch.common.logging.ESLogger;
@@ -37,8 +43,6 @@ import org.elasticsearch.common.logging.Loggers;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.util.concurrent.ThreadContext;
 import org.elasticsearch.common.util.set.Sets;
-import org.elasticsearch.test.rest.client.http.HttpRequestBuilder;
-import org.elasticsearch.test.rest.client.http.HttpResponse;
 import org.elasticsearch.test.rest.spec.RestApi;
 import org.elasticsearch.test.rest.spec.RestSpec;
 
@@ -46,6 +50,8 @@ import javax.net.ssl.SSLContext;
 import java.io.Closeable;
 import java.io.IOException;
 import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
 import java.net.URL;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -55,61 +61,54 @@ import java.security.KeyStoreException;
 import java.security.NoSuchAlgorithmException;
 import java.security.cert.CertificateException;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
-import static java.util.Objects.requireNonNull;
 
 /**
- * REST client used to test the elasticsearch REST layer
+ * REST client used to test the elasticsearch REST layer.
+ * Wraps a {@link RestClient} instance used to send the REST requests.
  * Holds the {@link RestSpec} used to translate api calls into REST calls
  */
-public class RestClient implements Closeable {
+public class RestTestClient implements Closeable {
 
     public static final String PROTOCOL = "protocol";
     public static final String TRUSTSTORE_PATH = "truststore.path";
     public static final String TRUSTSTORE_PASSWORD = "truststore.password";
 
-    private static final ESLogger logger = Loggers.getLogger(RestClient.class);
+
+    private static final ESLogger logger = Loggers.getLogger(RestTestClient.class);
     //query_string params that don't need to be declared in the spec, thay are supported by default
     private static final Set<String> ALWAYS_ACCEPTED_QUERY_STRING_PARAMS = Sets.newHashSet("pretty", "source", "filter_path");
 
-    private final String protocol;
     private final RestSpec restSpec;
-    private final CloseableHttpClient httpClient;
-    private final URL[] urls;
+    private final RestClient restClient;
     private final Version esVersion;
-    private final ThreadContext threadContext;
 
-    public RestClient(RestSpec restSpec, Settings settings, URL[] urls) throws IOException, RestException {
+    public RestTestClient(RestSpec restSpec, Settings settings, URL[] urls) throws IOException {
         assert urls.length > 0;
         this.restSpec = restSpec;
-        this.protocol = settings.get(PROTOCOL, "http");
-        this.httpClient = createHttpClient(settings);
-        this.threadContext = new ThreadContext(settings);
-        this.urls = urls;
-        this.esVersion = readAndCheckVersion();
+        this.restClient = createRestClient(urls, settings);
+        this.esVersion = readAndCheckVersion(urls);
         logger.info("REST client initialized {}, elasticsearch version: [{}]", urls, esVersion);
     }
 
-    private Version readAndCheckVersion() throws IOException, RestException {
-        //we make a manual call here without using callApi method, mainly because we are initializing
-        //and the randomized context doesn't exist for the current thread (would be used to choose the method otherwise)
+    private Version readAndCheckVersion(URL[] urls) throws IOException {
         RestApi restApi = restApi("info");
         assert restApi.getPaths().size() == 1;
         assert restApi.getMethods().size() == 1;
 
         String version = null;
-        for (URL url : urls) {
-            RestResponse restResponse = new RestResponse(httpRequestBuilder(url)
-                    .path(restApi.getPaths().get(0))
-                    .method(restApi.getMethods().get(0)).execute());
-            checkStatusCode(restResponse);
-
-            Object latestVersion = restResponse.evaluate("version.number");
+        for (URL ignored : urls) {
+            //we don't really use the urls here, we rely on the client doing round-robin to touch all the nodes in the cluster
+            String method = restApi.getMethods().get(0);
+            String endpoint = restApi.getPaths().get(0);
+            Response response = restClient.performRequest(method, endpoint, Collections.emptyMap(), null);
+            RestTestResponse restTestResponse = new RestTestResponse(response);
+            Object latestVersion = restTestResponse.evaluate("version.number");
             if (latestVersion == null) {
                 throw new RuntimeException("elasticsearch version not found in the response");
             }
@@ -130,77 +129,41 @@ public class RestClient implements Closeable {
 
     /**
      * Calls an api with the provided parameters and body
-     * @throws RestException if the obtained status code is non ok, unless the specific error code needs to be ignored
-     * according to the ignore parameter received as input (which won't get sent to elasticsearch)
      */
-    public RestResponse callApi(String apiName, Map<String, String> params, String body, Map<String, String> headers)
-            throws IOException, RestException {
+    public RestTestResponse callApi(String apiName, Map<String, String> params, String body, Map<String, String> headers)
+            throws IOException {
 
-        List<Integer> ignores = new ArrayList<>();
-        Map<String, String> requestParams = null;
-        if (params != null) {
-            //makes a copy of the parameters before modifying them for this specific request
-            requestParams = new HashMap<>(params);
-            //ignore is a special parameter supported by the clients, shouldn't be sent to es
-            String ignoreString = requestParams.remove("ignore");
-            if (Strings.hasLength(ignoreString)) {
-                try {
-                    ignores.add(Integer.valueOf(ignoreString));
-                } catch(NumberFormatException e) {
-                    throw new IllegalArgumentException("ignore value should be a number, found [" + ignoreString + "] instead");
-                }
-            }
-        }
-
-        HttpRequestBuilder httpRequestBuilder = callApiBuilder(apiName, requestParams, body);
-        for (Map.Entry<String, String> header : headers.entrySet()) {
-            logger.error("Adding header {}\n with value {}", header.getKey(), header.getValue());
-            httpRequestBuilder.addHeader(header.getKey(), header.getValue());
-        }
-        logger.debug("calling api [{}]", apiName);
-        HttpResponse httpResponse = httpRequestBuilder.execute();
-
-        // http HEAD doesn't support response body
-        // For the few api (exists class of api) that use it we need to accept 404 too
-        if (!httpResponse.supportsBody()) {
-            ignores.add(404);
-        }
-
-        RestResponse restResponse = new RestResponse(httpResponse);
-        checkStatusCode(restResponse, ignores);
-        return restResponse;
-    }
-
-    private void checkStatusCode(RestResponse restResponse, List<Integer> ignores) throws RestException {
-        //ignore is a catch within the client, to prevent the client from throwing error if it gets non ok codes back
-        if (ignores.contains(restResponse.getStatusCode())) {
-            if (logger.isDebugEnabled()) {
-                logger.debug("ignored non ok status codes {} as requested", ignores);
-            }
-            return;
-        }
-        checkStatusCode(restResponse);
-    }
-
-    private void checkStatusCode(RestResponse restResponse) throws RestException {
-        if (restResponse.isError()) {
-            throw new RestException("non ok status code [" + restResponse.getStatusCode() + "] returned", restResponse);
-        }
-    }
-
-    private HttpRequestBuilder callApiBuilder(String apiName, Map<String, String> params, String body) {
         if ("raw".equals(apiName)) {
             // Raw requests are bit simpler....
-            HttpRequestBuilder httpRequestBuilder = httpRequestBuilder();
-            httpRequestBuilder.method(requireNonNull(params.remove("method"), "Method must be set to use raw request"));
-            httpRequestBuilder.path("/"+ requireNonNull(params.remove("path"), "Path must be set to use raw request"));
-            httpRequestBuilder.body(body);
-
+            Map<String, String> queryStringParams = new HashMap<>(params);
+            String method = Objects.requireNonNull(queryStringParams.remove("method"), "Method must be set to use raw request");
+            String path = "/"+ Objects.requireNonNull(queryStringParams.remove("path"), "Path must be set to use raw request");
+            HttpEntity entity = null;
+            if (body != null && body.length() > 0) {
+                entity = new StringEntity(body, RestClient.JSON_CONTENT_TYPE);
+            }
             // And everything else is a url parameter!
-            for (Map.Entry<String, String> entry : params.entrySet()) {
-                httpRequestBuilder.addParam(entry.getKey(), entry.getValue());
+            Response response = restClient.performRequest(method, path, queryStringParams, entity);
+            return new RestTestResponse(response);
+        }
+
+        List<Integer> ignores = new ArrayList<>();
+        Map<String, String> requestParams;
+        if (params == null) {
+            requestParams = Collections.emptyMap();
+        } else {
+            requestParams = new HashMap<>(params);
+            if (params.isEmpty() == false) {
+                //ignore is a special parameter supported by the clients, shouldn't be sent to es
+                String ignoreString = requestParams.remove("ignore");
+                if (ignoreString != null) {
+                    try {
+                        ignores.add(Integer.valueOf(ignoreString));
+                    } catch (NumberFormatException e) {
+                        throw new IllegalArgumentException("ignore value should be a number, found [" + ignoreString + "] instead");
+                    }
+                }
             }
-            return httpRequestBuilder;
         }
 
         //create doesn't exist in the spec but is supported in the clients (index with op_type=create)
@@ -208,51 +171,91 @@ public class RestClient implements Closeable {
         String api = indexCreateApi ? "index" : apiName;
         RestApi restApi = restApi(api);
 
-        HttpRequestBuilder httpRequestBuilder = httpRequestBuilder();
-
         //divide params between ones that go within query string and ones that go within path
         Map<String, String> pathParts = new HashMap<>();
-        if (params != null) {
-            for (Map.Entry<String, String> entry : params.entrySet()) {
-                if (restApi.getPathParts().contains(entry.getKey())) {
-                    pathParts.put(entry.getKey(), entry.getValue());
+        Map<String, String> queryStringParams = new HashMap<>();
+        for (Map.Entry<String, String> entry : requestParams.entrySet()) {
+            if (restApi.getPathParts().contains(entry.getKey())) {
+                pathParts.put(entry.getKey(), entry.getValue());
+            } else {
+                if (restApi.getParams().contains(entry.getKey()) || ALWAYS_ACCEPTED_QUERY_STRING_PARAMS.contains(entry.getKey())) {
+                    queryStringParams.put(entry.getKey(), entry.getValue());
                 } else {
-                    if (restApi.getParams().contains(entry.getKey()) || ALWAYS_ACCEPTED_QUERY_STRING_PARAMS.contains(entry.getKey())) {
-                        httpRequestBuilder.addParam(entry.getKey(), entry.getValue());
-                    } else {
-                        throw new IllegalArgumentException("param [" + entry.getKey() +
-                                "] not supported in [" + restApi.getName() + "] api");
-                    }
+                    throw new IllegalArgumentException("param [" + entry.getKey() + "] not supported in ["
+                            + restApi.getName() + "] " + "api");
                 }
             }
         }
 
         if (indexCreateApi) {
-            httpRequestBuilder.addParam("op_type", "create");
+            queryStringParams.put("op_type", "create");
         }
 
         List<String> supportedMethods = restApi.getSupportedMethods(pathParts.keySet());
+        String requestMethod;
+        StringEntity requestBody = null;
         if (Strings.hasLength(body)) {
             if (!restApi.isBodySupported()) {
                 throw new IllegalArgumentException("body is not supported by [" + restApi.getName() + "] api");
             }
-            //test the GET with source param instead of GET/POST with body
+            //randomly test the GET with source param instead of GET/POST with body
             if (supportedMethods.contains("GET") && RandomizedTest.rarely()) {
                 logger.debug("sending the request body as source param with GET method");
-                httpRequestBuilder.addParam("source", body).method("GET");
+                queryStringParams.put("source", body);
+                requestMethod = "GET";
             } else {
-                httpRequestBuilder.body(body).method(RandomizedTest.randomFrom(supportedMethods));
+                requestMethod = RandomizedTest.randomFrom(supportedMethods);
+                requestBody = new StringEntity(body, RestClient.JSON_CONTENT_TYPE);
             }
         } else {
             if (restApi.isBodyRequired()) {
                 throw new IllegalArgumentException("body is required by [" + restApi.getName() + "] api");
             }
-            httpRequestBuilder.method(RandomizedTest.randomFrom(supportedMethods));
+            requestMethod = RandomizedTest.randomFrom(supportedMethods);
         }
 
         //the rest path to use is randomized out of the matching ones (if more than one)
         RestPath restPath = RandomizedTest.randomFrom(restApi.getFinalPaths(pathParts));
-        return httpRequestBuilder.pathParts(restPath.getPathParts());
+        //Encode rules for path and query string parameters are different. We use URI to encode the path.
+        //We need to encode each path part separately, as each one might contain slashes that need to be escaped, which needs to
+        //be done manually.
+        String requestPath;
+        if (restPath.getPathParts().length == 0) {
+            requestPath = "/";
+        } else {
+            StringBuilder finalPath = new StringBuilder();
+            for (String pathPart : restPath.getPathParts()) {
+                try {
+                    finalPath.append('/');
+                    // We append "/" to the path part to handle parts that start with - or other invalid characters
+                    URI uri = new URI(null, null, null, -1, "/" + pathPart, null, null);
+                    //manually escape any slash that each part may contain
+                    finalPath.append(uri.getRawPath().substring(1).replaceAll("/", "%2F"));
+                } catch (URISyntaxException e) {
+                    throw new RuntimeException("unable to build uri", e);
+                }
+            }
+            requestPath = finalPath.toString();
+        }
+
+        Header[] requestHeaders = new Header[headers.size()];
+        int index = 0;
+        for (Map.Entry<String, String> header : headers.entrySet()) {
+            logger.info("Adding header {}\n with value {}", header.getKey(), header.getValue());
+            requestHeaders[index++] = new BasicHeader(header.getKey(), header.getValue());
+        }
+
+        logger.debug("calling api [{}]", apiName);
+        try {
+            Response response = restClient.performRequest(requestMethod, requestPath,
+                    queryStringParams, requestBody, requestHeaders);
+            return new RestTestResponse(response);
+        } catch(ResponseException e) {
+            if (ignores.contains(e.getResponse().getStatusLine().getStatusCode())) {
+                return new RestTestResponse(e);
+            }
+            throw e;
+        }
     }
 
     private RestApi restApi(String apiName) {
@@ -263,21 +266,7 @@ public class RestClient implements Closeable {
         return restApi;
     }
 
-    protected HttpRequestBuilder httpRequestBuilder(URL url) {
-        return new HttpRequestBuilder(httpClient)
-                .addHeaders(threadContext.getHeaders())
-                .protocol(protocol)
-                .host(url.getHost())
-                .port(url.getPort());
-    }
-
-    protected HttpRequestBuilder httpRequestBuilder() {
-        //the address used is randomized between the available ones
-        URL url = RandomizedTest.randomFrom(urls);
-        return httpRequestBuilder(url);
-    }
-
-    protected CloseableHttpClient createHttpClient(Settings settings) throws IOException {
+    private static RestClient createRestClient(URL[] urls, Settings settings) throws IOException {
         SSLConnectionSocketFactory sslsf;
         String keystorePath = settings.get(TRUSTSTORE_PATH);
         if (keystorePath != null) {
@@ -307,8 +296,25 @@ public class RestClient implements Closeable {
                 .register("http", PlainConnectionSocketFactory.getSocketFactory())
                 .register("https", sslsf)
                 .build();
-        return HttpClients.createMinimal(
-                new PoolingHttpClientConnectionManager(socketFactoryRegistry, null, null, null, 15, TimeUnit.SECONDS));
+        CloseableHttpClient httpClient = RestClient.Builder.createDefaultHttpClient(socketFactoryRegistry);
+
+        String protocol = settings.get(PROTOCOL, "http");
+        HttpHost[] hosts = new HttpHost[urls.length];
+        for (int i = 0; i < hosts.length; i++) {
+            URL url = urls[i];
+            hosts[i] = new HttpHost(url.getHost(), url.getPort(), protocol);
+        }
+
+        RestClient.Builder builder = RestClient.builder(hosts).setHttpClient(httpClient);
+        try (ThreadContext threadContext = new ThreadContext(settings)) {
+            Header[] defaultHeaders = new Header[threadContext.getHeaders().size()];
+            int i = 0;
+            for (Map.Entry<String, String> entry : threadContext.getHeaders().entrySet()) {
+                defaultHeaders[i++] = new BasicHeader(entry.getKey(), entry.getValue());
+            }
+            builder.setDefaultHeaders(defaultHeaders);
+        }
+        return builder.build();
     }
 
     /**
@@ -316,6 +322,6 @@ public class RestClient implements Closeable {
      */
     @Override
     public void close() {
-        IOUtils.closeWhileHandlingException(httpClient);
+        IOUtils.closeWhileHandlingException(restClient);
     }
 }

+ 39 - 17
test/framework/src/main/java/org/elasticsearch/test/rest/client/RestResponse.java → test/framework/src/main/java/org/elasticsearch/test/rest/client/RestTestResponse.java

@@ -18,31 +18,54 @@
  */
 package org.elasticsearch.test.rest.client;
 
+import org.apache.http.client.methods.HttpHead;
+import org.apache.http.util.EntityUtils;
+import org.apache.lucene.util.IOUtils;
+import org.elasticsearch.client.Response;
+import org.elasticsearch.client.ResponseException;
 import org.elasticsearch.test.rest.Stash;
-import org.elasticsearch.test.rest.client.http.HttpResponse;
 import org.elasticsearch.test.rest.json.JsonPath;
 
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 
 /**
- * Response obtained from a REST call
- * Supports parsing the response body as json when needed and returning specific values extracted from it
+ * Response obtained from a REST call, eagerly reads the response body into a string for later optional parsing.
+ * Supports parsing the response body as json when needed and returning specific values extracted from it.
  */
-public class RestResponse {
+public class RestTestResponse {
 
-    private final HttpResponse response;
+    private final Response response;
+    private final String body;
     private JsonPath parsedResponse;
 
-    public RestResponse(HttpResponse response) {
+    public RestTestResponse(Response response) {
         this.response = response;
+        if (response.getEntity() != null) {
+            try {
+                this.body = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
+            } catch (IOException e) {
+                EntityUtils.consumeQuietly(response.getEntity());
+                throw new RuntimeException(e);
+            } finally {
+                IOUtils.closeWhileHandlingException(response);
+            }
+        } else {
+            this.body = null;
+        }
+    }
+
+    public RestTestResponse(ResponseException responseException) {
+        this.response = responseException.getResponse();
+        this.body = responseException.getResponseBody();
     }
 
     public int getStatusCode() {
-        return response.getStatusCode();
+        return response.getStatusLine().getStatusCode();
     }
 
     public String getReasonPhrase() {
-        return response.getReasonPhrase();
+        return response.getStatusLine().getReasonPhrase();
     }
 
     /**
@@ -57,18 +80,18 @@ public class RestResponse {
             }
             return parsedResponse.evaluate("");
         }
-        return response.getBody();
+        return body;
     }
 
     /**
      * Returns the body as a string
      */
     public String getBodyAsString() {
-        return response.getBody();
+        return body;
     }
 
     public boolean isError() {
-        return response.isError();
+        return response.getStatusLine().getStatusCode() >= 400;
     }
 
     /**
@@ -82,7 +105,6 @@ public class RestResponse {
      * Parses the response body as json and extracts a specific value from it (identified by the provided path)
      */
     public Object evaluate(String path, Stash stash) throws IOException {
-
         if (response == null) {
             return null;
         }
@@ -93,8 +115,8 @@ public class RestResponse {
             //special case: api that don't support body (e.g. exists) return true if 200, false if 404, even if no body
             //is_true: '' means the response had no body but the client returned true (caused by 200)
             //is_false: '' means the response had no body but the client returned false (caused by 404)
-            if ("".equals(path) && !response.supportsBody()) {
-                return !response.isError();
+            if ("".equals(path) && HttpHead.METHOD_NAME.equals(response.getRequestLine().getMethod())) {
+                return isError() == false;
             }
             return null;
         }
@@ -103,7 +125,7 @@ public class RestResponse {
     }
 
     private boolean isJson() {
-        String contentType = response.getHeaders().get("Content-Type");
+        String contentType = response.getHeader("Content-Type");
         return contentType != null && contentType.contains("application/json");
     }
 
@@ -111,9 +133,9 @@ public class RestResponse {
         if (parsedResponse != null) {
             return parsedResponse;
         }
-        if (response == null || !response.hasBody()) {
+        if (response == null || body == null) {
             return null;
         }
-        return parsedResponse = new JsonPath(response.getBody());
+        return parsedResponse = new JsonPath(body);
     }
 }

+ 0 - 250
test/framework/src/main/java/org/elasticsearch/test/rest/client/http/HttpRequestBuilder.java

@@ -1,250 +0,0 @@
-/*
- * Licensed to Elasticsearch under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.elasticsearch.test.rest.client.http;
-
-import org.apache.http.client.methods.CloseableHttpResponse;
-import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
-import org.apache.http.client.methods.HttpHead;
-import org.apache.http.client.methods.HttpOptions;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.client.methods.HttpPut;
-import org.apache.http.client.methods.HttpUriRequest;
-import org.apache.http.entity.StringEntity;
-import org.apache.http.impl.client.CloseableHttpClient;
-import org.elasticsearch.common.Strings;
-import org.elasticsearch.common.logging.ESLogger;
-import org.elasticsearch.common.logging.Loggers;
-import org.elasticsearch.common.network.NetworkAddress;
-import org.elasticsearch.common.transport.InetSocketTransportAddress;
-import org.elasticsearch.http.HttpServerTransport;
-
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URLEncoder;
-import java.nio.charset.Charset;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-/**
- * Executable builder for an http request
- * Holds an {@link org.apache.http.client.HttpClient} that is used to send the built http request
- */
-public class HttpRequestBuilder {
-
-    private static final ESLogger logger = Loggers.getLogger(HttpRequestBuilder.class);
-
-    static final Charset DEFAULT_CHARSET = Charset.forName("utf-8");
-
-    private final CloseableHttpClient httpClient;
-
-    private String protocol = "http";
-
-    private String host;
-
-    private int port;
-
-    private String path = "";
-
-    private final Map<String, String> params = new HashMap<>();
-
-    private final Map<String, String> headers = new HashMap<>();
-
-    private String method = HttpGetWithEntity.METHOD_NAME;
-
-    private String body;
-
-    public HttpRequestBuilder(CloseableHttpClient httpClient) {
-        this.httpClient = httpClient;
-    }
-
-    public HttpRequestBuilder host(String host) {
-        this.host = host;
-        return this;
-    }
-
-    public HttpRequestBuilder httpTransport(HttpServerTransport httpServerTransport) {
-        InetSocketTransportAddress transportAddress = (InetSocketTransportAddress) httpServerTransport.boundAddress().publishAddress();
-        return host(NetworkAddress.format(transportAddress.address().getAddress())).port(transportAddress.address().getPort());
-    }
-
-    public HttpRequestBuilder port(int port) {
-        this.port = port;
-        return this;
-    }
-
-    /**
-     * Sets the path to send the request to. Url encoding needs to be applied by the caller.
-     * Use {@link #pathParts(String...)} instead if the path needs to be encoded, part by part.
-     */
-    public HttpRequestBuilder path(String path) {
-        this.path = path;
-        return this;
-    }
-
-    /**
-     * Sets the path by providing the different parts (without slashes), which will be properly encoded.
-     */
-    public HttpRequestBuilder pathParts(String... path) {
-        //encode rules for path and query string parameters are different. We use URI to encode the path, and URLEncoder for each query string parameter (see addParam).
-        //We need to encode each path part separately though, as each one might contain slashes that need to be escaped, which needs to be done manually.
-        if (path.length == 0) {
-            this.path = "/";
-            return this;
-        }
-        StringBuilder finalPath = new StringBuilder();
-        for (String pathPart : path) {
-            try {
-                finalPath.append('/');
-                // We append "/" to the path part to handle parts that start with - or other invalid characters
-                URI uri = new URI(null, null, null, -1, "/" + pathPart, null, null);
-                //manually escape any slash that each part may contain
-                finalPath.append(uri.getRawPath().substring(1).replaceAll("/", "%2F"));
-            } catch(URISyntaxException e) {
-                throw new RuntimeException("unable to build uri", e);
-            }
-        }
-        this.path = finalPath.toString();
-        return this;
-    }
-
-    public HttpRequestBuilder addParam(String name, String value) {
-        try {
-            this.params.put(name, URLEncoder.encode(value, "utf-8"));
-            return this;
-        } catch (UnsupportedEncodingException e) {
-            throw new RuntimeException(e);
-        }
-    }
-
-    public HttpRequestBuilder addHeaders(Map<String, String> headers) {
-        this.headers.putAll(headers);
-        return this;
-    }
-
-    public HttpRequestBuilder addHeader(String name, String value) {
-        this.headers.put(name, value);
-        return this;
-    }
-
-    public HttpRequestBuilder protocol(String protocol) {
-        this.protocol = protocol;
-        return this;
-    }
-
-    public HttpRequestBuilder method(String method) {
-        this.method = method;
-        return this;
-    }
-
-    public HttpRequestBuilder body(String body) {
-        if (Strings.hasLength(body)) {
-            this.body = body;
-        }
-        return this;
-    }
-
-    public HttpResponse execute() throws IOException {
-        HttpUriRequest httpUriRequest = buildRequest();
-        if (logger.isTraceEnabled()) {
-            StringBuilder stringBuilder = new StringBuilder(httpUriRequest.getMethod()).append(" ").append(httpUriRequest.getURI());
-            if (Strings.hasLength(body)) {
-                stringBuilder.append("\n").append(body);
-            }
-            logger.trace("sending request \n{}", stringBuilder.toString());
-        }
-        for (Map.Entry<String, String> entry : this.headers.entrySet()) {
-            logger.trace("adding header [{} => {}]", entry.getKey(), entry.getValue());
-            httpUriRequest.addHeader(entry.getKey(), entry.getValue());
-        }
-        try (CloseableHttpResponse closeableHttpResponse = httpClient.execute(httpUriRequest)) {
-            HttpResponse httpResponse = new HttpResponse(httpUriRequest, closeableHttpResponse);
-            logger.trace("got response \n{}\n{}", closeableHttpResponse, httpResponse.hasBody() ? httpResponse.getBody() : "");
-            return httpResponse;
-        }
-    }
-
-    private HttpUriRequest buildRequest() {
-
-        if (HttpGetWithEntity.METHOD_NAME.equalsIgnoreCase(method)) {
-            return addOptionalBody(new HttpGetWithEntity(buildUri()));
-        }
-
-        if (HttpHead.METHOD_NAME.equalsIgnoreCase(method)) {
-            checkBodyNotSupported();
-            return new HttpHead(buildUri());
-        }
-
-        if (HttpOptions.METHOD_NAME.equalsIgnoreCase(method)) {
-            checkBodyNotSupported();
-            return new HttpOptions(buildUri());
-        }
-
-        if (HttpDeleteWithEntity.METHOD_NAME.equalsIgnoreCase(method)) {
-            return addOptionalBody(new HttpDeleteWithEntity(buildUri()));
-        }
-
-        if (HttpPut.METHOD_NAME.equalsIgnoreCase(method)) {
-            return addOptionalBody(new HttpPut(buildUri()));
-        }
-
-        if (HttpPost.METHOD_NAME.equalsIgnoreCase(method)) {
-            return addOptionalBody(new HttpPost(buildUri()));
-        }
-
-        throw new UnsupportedOperationException("method [" + method + "] not supported");
-    }
-
-    private URI buildUri() {
-        StringBuilder uriBuilder = new StringBuilder(protocol).append("://").append(host).append(":").append(port).append(path);
-        if (params.size() > 0) {
-            uriBuilder.append("?").append(params.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining("&")));
-        }
-        //using this constructor no url encoding happens, as we did everything upfront in addParam and pathPart methods
-        return URI.create(uriBuilder.toString());
-    }
-
-    private HttpEntityEnclosingRequestBase addOptionalBody(HttpEntityEnclosingRequestBase requestBase) {
-        if (Strings.hasText(body)) {
-            requestBase.setEntity(new StringEntity(body, DEFAULT_CHARSET));
-        }
-        return requestBase;
-    }
-
-    private void checkBodyNotSupported() {
-        if (Strings.hasText(body)) {
-            throw new IllegalArgumentException("request body not supported with head request");
-        }
-    }
-
-    @Override
-    public String toString() {
-        StringBuilder stringBuilder = new StringBuilder(method).append(" '")
-                .append(host).append(":").append(port).append(path).append("'");
-        if (!params.isEmpty()) {
-            stringBuilder.append(", params=").append(params);
-        }
-        if (Strings.hasLength(body)) {
-            stringBuilder.append(", body=\n").append(body);
-        }
-        return stringBuilder.toString();
-    }
-}

+ 0 - 108
test/framework/src/main/java/org/elasticsearch/test/rest/client/http/HttpResponse.java

@@ -1,108 +0,0 @@
-/*
- * Licensed to Elasticsearch under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.elasticsearch.test.rest.client.http;
-
-import org.apache.http.Header;
-import org.apache.http.client.methods.CloseableHttpResponse;
-import org.apache.http.client.methods.HttpHead;
-import org.apache.http.client.methods.HttpUriRequest;
-import org.apache.http.util.EntityUtils;
-import org.elasticsearch.common.logging.ESLogger;
-import org.elasticsearch.common.logging.Loggers;
-
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Response obtained from an http request
- * Always consumes the whole response body loading it entirely into a string
- */
-public class HttpResponse {
-
-    private static final ESLogger logger = Loggers.getLogger(HttpResponse.class);
-
-    private final HttpUriRequest httpRequest;
-    private final int statusCode;
-    private final String reasonPhrase;
-    private final String body;
-    private final Map<String, String> headers = new HashMap<>();
-
-    HttpResponse(HttpUriRequest httpRequest, CloseableHttpResponse httpResponse) {
-        this.httpRequest = httpRequest;
-        this.statusCode = httpResponse.getStatusLine().getStatusCode();
-        this.reasonPhrase = httpResponse.getStatusLine().getReasonPhrase();
-        for (Header header : httpResponse.getAllHeaders()) {
-            this.headers.put(header.getName(), header.getValue());
-        }
-        if (httpResponse.getEntity() != null) {
-            try {
-                this.body = EntityUtils.toString(httpResponse.getEntity(), HttpRequestBuilder.DEFAULT_CHARSET);
-            } catch (IOException e) {
-                EntityUtils.consumeQuietly(httpResponse.getEntity());
-                throw new RuntimeException(e);
-            } finally {
-                try {
-                    httpResponse.close();
-                } catch (IOException e) {
-                    logger.error("Failed closing response", e);
-                }
-            }
-        } else {
-            this.body = null;
-        }
-    }
-
-    public boolean isError() {
-        return statusCode >= 400;
-    }
-
-    public int getStatusCode() {
-        return statusCode;
-    }
-
-    public String getReasonPhrase() {
-        return reasonPhrase;
-    }
-
-    public String getBody() {
-        return body;
-    }
-
-    public boolean hasBody() {
-        return body != null;
-    }
-
-    public boolean supportsBody() {
-        return !HttpHead.METHOD_NAME.equals(httpRequest.getMethod());
-    }
-
-    public Map<String, String> getHeaders() {
-        return headers;
-    }
-
-    @Override
-    public String toString() {
-        StringBuilder stringBuilder = new StringBuilder(statusCode).append(" ").append(reasonPhrase);
-        if (hasBody()) {
-            stringBuilder.append("\n").append(body);
-        }
-        return stringBuilder.toString();
-    }
-}

+ 18 - 15
test/framework/src/main/java/org/elasticsearch/test/rest/section/DoSection.java

@@ -18,13 +18,13 @@
  */
 package org.elasticsearch.test.rest.section;
 
+import org.elasticsearch.client.ResponseException;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.collect.Tuple;
 import org.elasticsearch.common.logging.ESLogger;
 import org.elasticsearch.common.logging.Loggers;
 import org.elasticsearch.test.rest.RestTestExecutionContext;
-import org.elasticsearch.test.rest.client.RestException;
-import org.elasticsearch.test.rest.client.RestResponse;
+import org.elasticsearch.test.rest.client.RestTestResponse;
 
 import java.io.IOException;
 import java.util.HashMap;
@@ -89,7 +89,7 @@ public class DoSection implements ExecutableSection {
         }
 
         try {
-            RestResponse restResponse = executionContext.callApi(apiCallSection.getApi(), apiCallSection.getParams(),
+            RestTestResponse restTestResponse = executionContext.callApi(apiCallSection.getApi(), apiCallSection.getParams(),
                     apiCallSection.getBodies(), apiCallSection.getHeaders());
             if (Strings.hasLength(catchParam)) {
                 String catchStatusCode;
@@ -100,16 +100,18 @@ public class DoSection implements ExecutableSection {
                 } else {
                     throw new UnsupportedOperationException("catch value [" + catchParam + "] not supported");
                 }
-                fail(formatStatusCodeMessage(restResponse, catchStatusCode));
+                fail(formatStatusCodeMessage(restTestResponse, catchStatusCode));
             }
-        } catch(RestException e) {
+        } catch(ResponseException e) {
+            RestTestResponse restTestResponse = new RestTestResponse(e);
             if (!Strings.hasLength(catchParam)) {
-                fail(formatStatusCodeMessage(e.restResponse(), "2xx"));
+                fail(formatStatusCodeMessage(restTestResponse, "2xx"));
             } else if (catches.containsKey(catchParam)) {
-                assertStatusCode(e.restResponse());
+                assertStatusCode(restTestResponse);
             } else if (catchParam.length() > 2 && catchParam.startsWith("/") && catchParam.endsWith("/")) {
                 //the text of the error message matches regular expression
-                assertThat(formatStatusCodeMessage(e.restResponse(), "4xx|5xx"), e.statusCode(), greaterThanOrEqualTo(400));
+                assertThat(formatStatusCodeMessage(restTestResponse, "4xx|5xx"),
+                        e.getResponse().getStatusLine().getStatusCode(), greaterThanOrEqualTo(400));
                 Object error = executionContext.response("error");
                 assertThat("error was expected in the response", error, notNullValue());
                 //remove delimiters from regex
@@ -122,19 +124,19 @@ public class DoSection implements ExecutableSection {
         }
     }
 
-    private void assertStatusCode(RestResponse restResponse) {
+    private void assertStatusCode(RestTestResponse restTestResponse) {
         Tuple<String, org.hamcrest.Matcher<Integer>> stringMatcherTuple = catches.get(catchParam);
-        assertThat(formatStatusCodeMessage(restResponse, stringMatcherTuple.v1()),
-                restResponse.getStatusCode(), stringMatcherTuple.v2());
+        assertThat(formatStatusCodeMessage(restTestResponse, stringMatcherTuple.v1()),
+                restTestResponse.getStatusCode(), stringMatcherTuple.v2());
     }
 
-    private String formatStatusCodeMessage(RestResponse restResponse, String expected) {
+    private String formatStatusCodeMessage(RestTestResponse restTestResponse, String expected) {
         String api = apiCallSection.getApi();
         if ("raw".equals(api)) {
             api += "[method=" + apiCallSection.getParams().get("method") + " path=" + apiCallSection.getParams().get("path") + "]";
         }
-        return "expected [" + expected + "] status code but api [" + api + "] returned ["
-                + restResponse.getStatusCode() + " " + restResponse.getReasonPhrase() + "] [" + restResponse.getBodyAsString() + "]";
+        return "expected [" + expected + "] status code but api [" + api + "] returned [" + restTestResponse.getStatusCode() +
+                " " + restTestResponse.getReasonPhrase() + "] [" + restTestResponse.getBodyAsString() + "]";
     }
 
     private static Map<String, Tuple<String, org.hamcrest.Matcher<Integer>>> catches = new HashMap<>();
@@ -145,6 +147,7 @@ public class DoSection implements ExecutableSection {
         catches.put("forbidden", tuple("403", equalTo(403)));
         catches.put("request_timeout", tuple("408", equalTo(408)));
         catches.put("unavailable", tuple("503", equalTo(503)));
-        catches.put("request", tuple("4xx|5xx", allOf(greaterThanOrEqualTo(400), not(equalTo(404)), not(equalTo(408)), not(equalTo(409)), not(equalTo(403)))));
+        catches.put("request", tuple("4xx|5xx",
+                allOf(greaterThanOrEqualTo(400), not(equalTo(404)), not(equalTo(408)), not(equalTo(409)), not(equalTo(403)))));
     }
 }