Browse Source

HLREST: Add x-pack-info API (#31870)

This is the first x-pack API we're adding to the high level REST client
so there is a lot to talk about here!

= Open source

The *client* for these APIs is open source. We're taking the previously
Elastic licensed files used for the `Request` and `Response` objects and
relicensing them under the Apache 2 license.

The implementation of these features is staying under the Elastic
license. This lines up with how the rest of the Elasticsearch language
clients work.

= Location of the new files

We're moving all of the `Request` and `Response` objects that we're
relicensing to the `x-pack/protocol` directory. We're adding a copy of
the Apache 2 license to the root fo the `x-pack/protocol` directory to
line up with the language in the root `LICENSE.txt` file. All files in
this directory will have the Apache 2 license header as well. We don't
want there to be any confusion. Even though the files are under the
`x-pack` directory, they are Apache 2 licensed.

We chose this particular directory layout because it keeps the X-Pack
stuff together and easier to think about.

= Location of the API in the REST client

We've been following the layout of the rest-api-spec files for other
APIs and we plan to do this for the X-Pack APIs with one exception:
we're dropping the `xpack` from the name of most of the APIs. So
`xpack.graph.explore` will become `graph().explore()` and
`xpack.license.get` will become `license().get()`.

`xpack.info` and `xpack.usage` are special here though because they
don't belong to any proper category. For now I'm just calling
`xpack.info` `xPackInfo()` and intend to call usage `xPackUsage` though
I'm not convinced that this is the final name for them. But it does get
us started.

= Jars, jars everywhere!

This change makes the `xpack:protocol` project a `compile` scoped
dependency of the `x-pack:plugin:core` and `client:rest-high-level`
projects. I intend to keep it a compile scoped dependency of
`x-pack:plugin:core` but I intend to bundle the contents of the protocol
jar into the `client:rest-high-level` jar in a follow up. This change
has grown large enough at this point.

In that followup I'll address javadoc issues as well.

= Breaking-Java

This breaks that transport client by a few classes around. We've
traditionally been ok with doing this to the transport client.
Nik Everett 7 years ago
parent
commit
fb27f3e7f0
34 changed files with 1429 additions and 425 deletions
  1. 1 0
      client/rest-high-level/build.gradle
  2. 16 0
      client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java
  3. 35 5
      client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java
  4. 51 1
      client/rest-high-level/src/test/java/org/elasticsearch/client/PingAndInfoIT.java
  5. 33 0
      client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java
  6. 63 0
      client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MiscellaneousDocumentationIT.java
  7. 64 0
      docs/java-rest/high-level/miscellaneous/x-pack-info.asciidoc
  8. 2 1
      docs/java-rest/high-level/supported-apis.asciidoc
  9. 1 0
      libs/x-content/src/main/java/org/elasticsearch/common/xcontent/ObjectParser.java
  10. 13 10
      x-pack/build.gradle
  11. 1 0
      x-pack/plugin/core/build.gradle
  12. 5 39
      x-pack/plugin/core/src/main/java/org/elasticsearch/license/License.java
  13. 3 1
      x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java
  14. 0 298
      x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackInfoResponse.java
  15. 2 2
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClient.java
  16. 8 5
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/action/TransportXPackInfoAction.java
  17. 1 1
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/action/XPackInfoAction.java
  18. 2 1
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/action/XPackInfoRequestBuilder.java
  19. 3 38
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rest/action/RestXPackInfoAction.java
  20. 5 3
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/action/TransportXPackInfoActionTests.java
  21. 5 4
      x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/MlRemoteLicenseChecker.java
  22. 9 9
      x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/MlRemoteLicenseCheckerTests.java
  23. 3 3
      x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolTests.java
  24. 202 0
      x-pack/protocol/LICENSE.txt
  25. 29 0
      x-pack/protocol/build.gradle
  26. 67 0
      x-pack/protocol/src/main/java/org/elasticsearch/protocol/license/LicenseStatus.java
  27. 24 0
      x-pack/protocol/src/main/java/org/elasticsearch/protocol/license/package-info.java
  28. 24 0
      x-pack/protocol/src/main/java/org/elasticsearch/protocol/security/package-info.java
  29. 24 0
      x-pack/protocol/src/main/java/org/elasticsearch/protocol/watcher/package-info.java
  30. 20 4
      x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/XPackInfoRequest.java
  31. 500 0
      x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/XPackInfoResponse.java
  32. 23 0
      x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/package-info.java
  33. 30 0
      x-pack/protocol/src/test/java/org/elasticsearch/protocol/license/LicenseStatusTests.java
  34. 160 0
      x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/XPackInfoResponseTests.java

+ 1 - 0
client/rest-high-level/build.gradle

@@ -41,6 +41,7 @@ dependencies {
   compile "org.elasticsearch.plugin:aggs-matrix-stats-client:${version}"
   compile "org.elasticsearch.plugin:rank-eval-client:${version}"
   compile "org.elasticsearch.plugin:lang-mustache-client:${version}"
+  compile project(':x-pack:protocol') // TODO bundle into the jar
 
   testCompile "org.elasticsearch.client:test:${version}"
   testCompile "org.elasticsearch.test:framework:${version}"

+ 16 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java

@@ -104,6 +104,7 @@ import org.elasticsearch.common.xcontent.XContentParser;
 import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.index.VersionType;
 import org.elasticsearch.index.rankeval.RankEvalRequest;
+import org.elasticsearch.protocol.xpack.XPackInfoRequest;
 import org.elasticsearch.rest.action.search.RestSearchAction;
 import org.elasticsearch.script.mustache.MultiSearchTemplateRequest;
 import org.elasticsearch.script.mustache.SearchTemplateRequest;
@@ -115,8 +116,10 @@ import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.nio.charset.Charset;
+import java.util.EnumSet;
 import java.util.Locale;
 import java.util.StringJoiner;
+import java.util.stream.Collectors;
 
 final class RequestConverters {
     static final XContentType REQUEST_BODY_CONTENT_TYPE = XContentType.JSON;
@@ -1065,6 +1068,19 @@ final class RequestConverters {
         return request;
     }
 
+    static Request xPackInfo(XPackInfoRequest infoRequest) {
+        Request request = new Request(HttpGet.METHOD_NAME, "/_xpack");
+        if (false == infoRequest.isVerbose()) {
+            request.addParameter("human", "false");
+        }
+        if (false == infoRequest.getCategories().equals(EnumSet.allOf(XPackInfoRequest.Category.class))) {
+            request.addParameter("categories", infoRequest.getCategories().stream()
+                    .map(c -> c.toString().toLowerCase(Locale.ROOT))
+                    .collect(Collectors.joining(",")));
+        }
+        return request;
+    }
+
     private static HttpEntity createEntity(ToXContent toXContent, XContentType xContentType) throws IOException {
         BytesRef source = XContentHelper.toXContent(toXContent, xContentType, false).toBytesRef();
         return new ByteArrayEntity(source.bytes, source.offset, source.length, createContentType(xContentType));

+ 35 - 5
client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java

@@ -66,6 +66,8 @@ import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.index.rankeval.RankEvalRequest;
 import org.elasticsearch.index.rankeval.RankEvalResponse;
 import org.elasticsearch.plugins.spi.NamedXContentProvider;
+import org.elasticsearch.protocol.xpack.XPackInfoRequest;
+import org.elasticsearch.protocol.xpack.XPackInfoResponse;
 import org.elasticsearch.rest.BytesRestResponse;
 import org.elasticsearch.rest.RestStatus;
 import org.elasticsearch.script.mustache.MultiSearchTemplateRequest;
@@ -668,7 +670,7 @@ public class RestHighLevelClient implements Closeable {
                 emptySet());
     }
 
-        
+
     /**
      * Executes a request using the Multi Search Template API.
      *
@@ -678,9 +680,9 @@ public class RestHighLevelClient implements Closeable {
     public final MultiSearchTemplateResponse multiSearchTemplate(MultiSearchTemplateRequest multiSearchTemplateRequest,
             RequestOptions options) throws IOException {
         return performRequestAndParseEntity(multiSearchTemplateRequest, RequestConverters::multiSearchTemplate,
-                options, MultiSearchTemplateResponse::fromXContext, emptySet());        
-    }   
-    
+                options, MultiSearchTemplateResponse::fromXContext, emptySet());
+    }
+
     /**
      * Asynchronously executes a request using the Multi Search Template API
      *
@@ -692,7 +694,7 @@ public class RestHighLevelClient implements Closeable {
                                           ActionListener<MultiSearchTemplateResponse> listener) {
         performRequestAsyncAndParseEntity(multiSearchTemplateRequest, RequestConverters::multiSearchTemplate,
             options, MultiSearchTemplateResponse::fromXContext, listener, emptySet());
-    }    
+    }
 
     /**
      * Asynchronously executes a request using the Ranking Evaluation API.
@@ -792,6 +794,34 @@ public class RestHighLevelClient implements Closeable {
             FieldCapabilitiesResponse::fromXContent, listener, emptySet());
     }
 
+    /**
+     * Fetch information about X-Pack from the cluster if it is installed.
+     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/info-api.html">
+     * the docs</a> for more.
+     * @param request the request
+     * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+     * @return the response
+     * @throws IOException in case there is a problem sending the request or parsing back the response
+     */
+    public XPackInfoResponse xPackInfo(XPackInfoRequest request, RequestOptions options) throws IOException {
+        return performRequestAndParseEntity(request, RequestConverters::xPackInfo, options,
+            XPackInfoResponse::fromXContent, emptySet());
+    }
+
+    /**
+     * Fetch information about X-Pack from the cluster if it is installed.
+     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/info-api.html">
+     * the docs</a> for more.
+     * @param request the request
+     * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+     * @param listener the listener to be notified upon request completion
+     */
+    public void xPackInfoAsync(XPackInfoRequest request, RequestOptions options,
+                                  ActionListener<XPackInfoResponse> listener) {
+        performRequestAsyncAndParseEntity(request, RequestConverters::xPackInfo, options,
+            XPackInfoResponse::fromXContent, listener, emptySet());
+    }
+
     protected final <Req extends ActionRequest, Resp> Resp performRequestAndParseEntity(Req request,
                                                                             CheckedFunction<Req, Request, IOException> requestConverter,
                                                                             RequestOptions options,

+ 51 - 1
client/rest-high-level/src/test/java/org/elasticsearch/client/PingAndInfoIT.java

@@ -21,8 +21,13 @@ package org.elasticsearch.client;
 
 import org.apache.http.client.methods.HttpGet;
 import org.elasticsearch.action.main.MainResponse;
+import org.elasticsearch.protocol.license.LicenseStatus;
+import org.elasticsearch.protocol.xpack.XPackInfoRequest;
+import org.elasticsearch.protocol.xpack.XPackInfoResponse;
+import org.elasticsearch.protocol.xpack.XPackInfoResponse.FeatureSetsInfo.FeatureSet;
 
 import java.io.IOException;
+import java.util.EnumSet;
 import java.util.Map;
 
 public class PingAndInfoIT extends ESRestHighLevelClientTestCase {
@@ -31,7 +36,6 @@ public class PingAndInfoIT extends ESRestHighLevelClientTestCase {
         assertTrue(highLevelClient().ping(RequestOptions.DEFAULT));
     }
 
-    @SuppressWarnings("unchecked")
     public void testInfo() throws IOException {
         MainResponse info = highLevelClient().info(RequestOptions.DEFAULT);
         // compare with what the low level client outputs
@@ -41,6 +45,7 @@ public class PingAndInfoIT extends ESRestHighLevelClientTestCase {
 
         // only check node name existence, might be a different one from what was hit by low level client in multi-node cluster
         assertNotNull(info.getNodeName());
+        @SuppressWarnings("unchecked")
         Map<String, Object> versionMap = (Map<String, Object>) infoAsMap.get("version");
         assertEquals(versionMap.get("build_flavor"), info.getBuild().flavor().displayName());
         assertEquals(versionMap.get("build_type"), info.getBuild().type().displayName());
@@ -51,4 +56,49 @@ public class PingAndInfoIT extends ESRestHighLevelClientTestCase {
         assertEquals(versionMap.get("lucene_version"), info.getVersion().luceneVersion.toString());
     }
 
+    public void testXPackInfo() throws IOException {
+        XPackInfoRequest request = new XPackInfoRequest();
+        request.setCategories(EnumSet.allOf(XPackInfoRequest.Category.class));
+        request.setVerbose(true);
+        XPackInfoResponse info = highLevelClient().xPackInfo(request, RequestOptions.DEFAULT);
+
+        MainResponse mainResponse = highLevelClient().info(RequestOptions.DEFAULT);
+
+        assertEquals(mainResponse.getBuild().shortHash(), info.getBuildInfo().getHash());
+
+        assertEquals("basic", info.getLicenseInfo().getType());
+        assertEquals("basic", info.getLicenseInfo().getMode());
+        assertEquals(LicenseStatus.ACTIVE, info.getLicenseInfo().getStatus());
+
+        FeatureSet graph = info.getFeatureSetsInfo().getFeatureSets().get("graph");
+        assertNotNull(graph.description());
+        assertFalse(graph.available());
+        assertTrue(graph.enabled());
+        assertNull(graph.nativeCodeInfo());
+        FeatureSet monitoring = info.getFeatureSetsInfo().getFeatureSets().get("monitoring");
+        assertNotNull(monitoring.description());
+        assertTrue(monitoring.available());
+        assertTrue(monitoring.enabled());
+        assertNull(monitoring.nativeCodeInfo());
+        FeatureSet ml = info.getFeatureSetsInfo().getFeatureSets().get("ml");
+        assertNotNull(ml.description());
+        assertFalse(ml.available());
+        assertTrue(ml.enabled());
+        assertEquals(mainResponse.getVersion().toString(),
+                ml.nativeCodeInfo().get("version").toString().replace("-SNAPSHOT", ""));
+    }
+
+    public void testXPackInfoEmptyRequest() throws IOException {
+        XPackInfoResponse info = highLevelClient().xPackInfo(new XPackInfoRequest(), RequestOptions.DEFAULT);
+
+        /*
+         * The default in the transport client is non-verbose and returning
+         * no categories which is the opposite of the default when you use
+         * the API over REST. We don't want to break the transport client
+         * even though it doesn't feel like a good default.
+         */
+        assertNull(info.getBuildInfo());
+        assertNull(info.getLicenseInfo());
+        assertNull(info.getFeatureSetsInfo());
+    }
 }

+ 33 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java

@@ -123,6 +123,7 @@ import org.elasticsearch.index.rankeval.RankEvalRequest;
 import org.elasticsearch.index.rankeval.RankEvalSpec;
 import org.elasticsearch.index.rankeval.RatedRequest;
 import org.elasticsearch.index.rankeval.RestRankEvalAction;
+import org.elasticsearch.protocol.xpack.XPackInfoRequest;
 import org.elasticsearch.repositories.fs.FsRepository;
 import org.elasticsearch.rest.action.search.RestSearchAction;
 import org.elasticsearch.script.ScriptType;
@@ -150,6 +151,7 @@ import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -2465,6 +2467,37 @@ public class RequestConvertersTests extends ESTestCase {
                 + "previous requests have content-type [" + xContentType + "]", exception.getMessage());
     }
 
+    public void testXPackInfo() {
+        XPackInfoRequest infoRequest = new XPackInfoRequest();
+        Map<String, String> expectedParams = new HashMap<>();
+        infoRequest.setVerbose(randomBoolean());
+        if (false == infoRequest.isVerbose()) {
+            expectedParams.put("human", "false");
+        }
+        int option = between(0, 2);
+        switch (option) {
+        case 0:
+            infoRequest.setCategories(EnumSet.allOf(XPackInfoRequest.Category.class));
+            break;
+        case 1:
+            infoRequest.setCategories(EnumSet.of(XPackInfoRequest.Category.FEATURES));
+            expectedParams.put("categories", "features");
+            break;
+        case 2:
+            infoRequest.setCategories(EnumSet.of(XPackInfoRequest.Category.FEATURES, XPackInfoRequest.Category.BUILD));
+            expectedParams.put("categories", "build,features");
+            break;
+        default:
+            throw new IllegalArgumentException("invalid option [" + option + "]");
+        }
+
+        Request request = RequestConverters.xPackInfo(infoRequest);
+        assertEquals(HttpGet.METHOD_NAME, request.getMethod());
+        assertEquals("/_xpack", request.getEndpoint());
+        assertNull(request.getEntity());
+        assertEquals(expectedParams, request.getParameters());
+    }
+
     /**
      * Randomize the {@link FetchSourceContext} request parameters.
      */

+ 63 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MiscellaneousDocumentationIT.java

@@ -22,14 +22,24 @@ package org.elasticsearch.client.documentation;
 import org.apache.http.HttpHost;
 import org.elasticsearch.Build;
 import org.elasticsearch.Version;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.LatchedActionListener;
 import org.elasticsearch.action.main.MainResponse;
 import org.elasticsearch.client.ESRestHighLevelClientTestCase;
 import org.elasticsearch.client.RequestOptions;
 import org.elasticsearch.client.RestClient;
 import org.elasticsearch.client.RestHighLevelClient;
 import org.elasticsearch.cluster.ClusterName;
+import org.elasticsearch.protocol.xpack.XPackInfoRequest;
+import org.elasticsearch.protocol.xpack.XPackInfoResponse;
+import org.elasticsearch.protocol.xpack.XPackInfoResponse.BuildInfo;
+import org.elasticsearch.protocol.xpack.XPackInfoResponse.FeatureSetsInfo;
+import org.elasticsearch.protocol.xpack.XPackInfoResponse.LicenseInfo;
 
 import java.io.IOException;
+import java.util.EnumSet;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Documentation for miscellaneous APIs in the high level java client.
@@ -66,6 +76,59 @@ public class MiscellaneousDocumentationIT extends ESRestHighLevelClientTestCase
         assertTrue(response);
     }
 
+    public void testXPackInfo() throws Exception {
+        RestHighLevelClient client = highLevelClient();
+        {
+            //tag::x-pack-info-execute
+            XPackInfoRequest request = new XPackInfoRequest();
+            request.setVerbose(true);          // <1>
+            request.setCategories(EnumSet.of(  // <2>
+                    XPackInfoRequest.Category.BUILD,
+                    XPackInfoRequest.Category.LICENSE,
+                    XPackInfoRequest.Category.FEATURES));
+            XPackInfoResponse response = client.xPackInfo(request, RequestOptions.DEFAULT);
+            //end::x-pack-info-execute
+
+            //tag::x-pack-info-response
+            BuildInfo build = response.getBuildInfo();                 // <1>
+            LicenseInfo license = response.getLicenseInfo();           // <2>
+            assertEquals(XPackInfoResponse.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS,
+                    license.getExpiryDate());                          // <3>
+            FeatureSetsInfo features = response.getFeatureSetsInfo();  // <4>
+            //end::x-pack-info-response
+
+            assertNotNull(response.getBuildInfo());
+            assertNotNull(response.getLicenseInfo());
+            assertNotNull(response.getFeatureSetsInfo());
+        }
+        {
+            XPackInfoRequest request = new XPackInfoRequest();
+            // tag::x-pack-info-execute-listener
+            ActionListener<XPackInfoResponse> listener = new ActionListener<XPackInfoResponse>() {
+                @Override
+                public void onResponse(XPackInfoResponse indexResponse) {
+                    // <1>
+                }
+
+                @Override
+                public void onFailure(Exception e) {
+                    // <2>
+                }
+            };
+            // end::x-pack-info-execute-listener
+
+            // Replace the empty listener by a blocking listener in test
+            final CountDownLatch latch = new CountDownLatch(1);
+            listener = new LatchedActionListener<>(listener, latch);
+
+            // tag::x-pack-info-execute-async
+            client.xPackInfoAsync(request, RequestOptions.DEFAULT, listener); // <1>
+            // end::x-pack-info-execute-async
+
+            assertTrue(latch.await(30L, TimeUnit.SECONDS));
+        }
+    }
+
     public void testInitializationFromClientBuilder() throws IOException {
         //tag::rest-high-level-client-init
         RestHighLevelClient client = new RestHighLevelClient(

+ 64 - 0
docs/java-rest/high-level/miscellaneous/x-pack-info.asciidoc

@@ -0,0 +1,64 @@
+[[java-rest-high-x-pack-info]]
+=== X-Pack Info API
+
+[[java-rest-high-x-pack-info-execution]]
+==== Execution
+
+General information about the installed {xpack} features can be retrieved
+using the `xPackInfo()` method:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/MiscellaneousDocumentationIT.java[x-pack-info-execute]
+--------------------------------------------------
+<1> Enable verbose mode. The default is `false` but `true` will return
+more information.
+<2> Set the categories of information to retrieve. The the default is to
+return no information which is useful for checking if {xpack} is installed
+but not much else.
+
+[[java-rest-high-x-pack-info-response]]
+==== Response
+
+The returned `XPackInfoResponse` can contain `BuildInfo`, `LicenseInfo`,
+and `FeatureSetsInfo` depending on the categories requested.
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/MiscellaneousDocumentationIT.java[x-pack-info-response]
+--------------------------------------------------
+<1> `BuildInfo` contains the commit hash from which Elasticsearch was
+built and the timestamp that the x-pack module was created.
+<2> `LicenseInfo` contains the type of license that the cluster is using
+and its expiration date.
+<3> Basic licenses do not expire and will return this constant.
+<4> `FeatureSetsInfo` contains a `Map` from the name of a feature to
+information about a feature like whether or not it is available under
+the current license.
+
+[[java-rest-high-x-pack-info-async]]
+==== Asynchronous Execution
+
+This request can be executed asynchronously:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/MiscellaneousDocumentationIT.java[x-pack-info-execute-async]
+--------------------------------------------------
+<1> The `XPackInfoRequest` to execute and the `ActionListener` to use when
+the execution completes
+
+The asynchronous method does not block and returns immediately. Once it is
+completed the `ActionListener` is called back using the `onResponse` method
+if the execution successfully completed or using the `onFailure` method if
+it failed.
+
+A typical listener for `XPackInfoResponse` looks like:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/MiscellaneousDocumentationIT.java[x-pack-info-execute-listener]
+--------------------------------------------------
+<1> Called when the execution is successfully completed. The response is
+provided as an argument
+<2> Called in case of failure. The raised exception is provided as an argument

+ 2 - 1
docs/java-rest/high-level/supported-apis.asciidoc

@@ -53,9 +53,11 @@ The Java High Level REST Client supports the following Miscellaneous APIs:
 
 * <<java-rest-high-main>>
 * <<java-rest-high-ping>>
+* <<java-rest-high-x-pack-info>>
 
 include::miscellaneous/main.asciidoc[]
 include::miscellaneous/ping.asciidoc[]
+include::miscellaneous/x-pack-info.asciidoc[]
 
 == Indices APIs
 
@@ -181,4 +183,3 @@ The Java High Level REST Client supports the following Scripts APIs:
 
 include::script/get_script.asciidoc[]
 include::script/delete_script.asciidoc[]
-

+ 1 - 0
libs/x-content/src/main/java/org/elasticsearch/common/xcontent/ObjectParser.java

@@ -411,6 +411,7 @@ public final class ObjectParser<Value, Context> extends AbstractObjectParser<Val
         INT_ARRAY(START_ARRAY, VALUE_NUMBER, VALUE_STRING),
         BOOLEAN_ARRAY(START_ARRAY, VALUE_BOOLEAN),
         OBJECT(START_OBJECT),
+        OBJECT_OR_NULL(START_OBJECT, VALUE_NULL),
         OBJECT_ARRAY(START_OBJECT, START_ARRAY),
         OBJECT_OR_BOOLEAN(START_OBJECT, VALUE_BOOLEAN),
         OBJECT_OR_STRING(START_OBJECT, VALUE_STRING),

+ 13 - 10
x-pack/build.gradle

@@ -12,16 +12,23 @@ subprojects {
   // helper method to find the path to a module
   ext.xpackModule = { String moduleName -> xpackProject("plugin:${moduleName}").path }
 
-  ext.licenseName = 'Elastic License'
-  ext.licenseUrl = ext.elasticLicenseUrl
-
-  project.ext.licenseFile = rootProject.file('licenses/ELASTIC-LICENSE.txt')
-  project.ext.noticeFile = xpackRootProject.file('NOTICE.txt')
-
   plugins.withType(PluginBuildPlugin).whenPluginAdded {
     project.esplugin.licenseFile = rootProject.file('licenses/ELASTIC-LICENSE.txt')
     project.esplugin.noticeFile = xpackRootProject.file('NOTICE.txt')
   }
+
+  if (project.name != 'protocol') {
+    tasks.withType(LicenseHeadersTask.class) {
+        approvedLicenses = ['Elastic License', 'Generated']
+        additionalLicense 'ELAST', 'Elastic License', 'Licensed under the Elastic License'
+    }
+
+    ext.licenseName = 'Elastic License'
+    ext.licenseUrl = ext.elasticLicenseUrl
+
+    project.ext.licenseFile = rootProject.file('licenses/ELASTIC-LICENSE.txt')
+    project.ext.noticeFile = xpackRootProject.file('NOTICE.txt')
+  }
 }
 
 File checkstyleSuppressions = file('dev-tools/checkstyle_suppressions.xml')
@@ -34,10 +41,6 @@ subprojects {
     ]
   }
 
-  tasks.withType(LicenseHeadersTask.class) {
-    approvedLicenses = ['Elastic License', 'Generated']
-    additionalLicense 'ELAST', 'Elastic License', 'Licensed under the Elastic License'
-  }
   ext.projectSubstitutions += [ "org.elasticsearch.plugin:x-pack-core:${version}": xpackModule('core')]
   ext.projectSubstitutions += [ "org.elasticsearch.plugin:x-pack-deprecation:${version}": xpackModule('deprecation')]
   ext.projectSubstitutions += [ "org.elasticsearch.plugin:x-pack-graph:${version}": xpackModule('graph')]

+ 1 - 0
x-pack/plugin/core/build.gradle

@@ -25,6 +25,7 @@ dependencyLicenses {
 
 dependencies {
     compileOnly "org.elasticsearch:elasticsearch:${version}"
+    compile project(':x-pack:protocol')
     compile "org.apache.httpcomponents:httpclient:${versions.httpclient}"
     compile "org.apache.httpcomponents:httpcore:${versions.httpcore}"
     compile "org.apache.httpcomponents:httpcore-nio:${versions.httpcore}"

+ 5 - 39
x-pack/plugin/core/src/main/java/org/elasticsearch/license/License.java

@@ -29,6 +29,7 @@ import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentFactory;
 import org.elasticsearch.common.xcontent.XContentParser;
 import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.protocol.license.LicenseStatus;
 
 /**
  * Data structure for license. Use {@link Builder} to build a license.
@@ -267,14 +268,14 @@ public class License implements ToXContentObject {
     /**
      * @return the current license's status
      */
-    public Status status() {
+    public LicenseStatus status() {
         long now = System.currentTimeMillis();
         if (issueDate > now) {
-            return Status.INVALID;
+            return LicenseStatus.INVALID;
         } else if (expiryDate < now) {
-            return Status.EXPIRED;
+            return LicenseStatus.EXPIRED;
         }
-        return Status.ACTIVE;
+        return LicenseStatus.ACTIVE;
     }
 
     private void validate() {
@@ -767,41 +768,6 @@ public class License implements ToXContentObject {
         }
     }
 
-    public enum Status {
-
-        ACTIVE("active"),
-        INVALID("invalid"),
-        EXPIRED("expired");
-
-        private final String label;
-
-        Status(String label) {
-            this.label = label;
-        }
-
-        public String label() {
-            return label;
-        }
-
-        public void writeTo(StreamOutput out) throws IOException {
-            out.writeString(label);
-        }
-
-        public static Status readFrom(StreamInput in) throws IOException {
-            String value = in.readString();
-            switch (value) {
-                case "active":
-                    return ACTIVE;
-                case "invalid":
-                    return INVALID;
-                case "expired":
-                    return EXPIRED;
-                default:
-                    throw new IllegalArgumentException("unknown license status [" + value + "]");
-            }
-        }
-    }
-
     /**
      * Returns <code>true</code> iff the license is a production licnese
      */

+ 3 - 1
x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java

@@ -27,6 +27,7 @@ import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.discovery.DiscoveryModule;
 import org.elasticsearch.env.Environment;
 import org.elasticsearch.gateway.GatewayService;
+import org.elasticsearch.protocol.xpack.XPackInfoResponse;
 import org.elasticsearch.watcher.ResourceWatcherService;
 import org.elasticsearch.xpack.core.XPackPlugin;
 import org.elasticsearch.xpack.core.XPackSettings;
@@ -72,7 +73,8 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
      */
     static final TimeValue GRACE_PERIOD_DURATION = days(7);
 
-    public static final long BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS = Long.MAX_VALUE - days(365).millis();
+    public static final long BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS =
+            XPackInfoResponse.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS;
 
     private final ClusterService clusterService;
 

+ 0 - 298
x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackInfoResponse.java

@@ -1,298 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-package org.elasticsearch.license;
-
-import org.elasticsearch.Version;
-import org.elasticsearch.action.ActionResponse;
-import org.elasticsearch.common.Nullable;
-import org.elasticsearch.common.io.stream.StreamInput;
-import org.elasticsearch.common.io.stream.StreamOutput;
-import org.elasticsearch.common.io.stream.Writeable;
-import org.elasticsearch.common.xcontent.ToXContentObject;
-import org.elasticsearch.common.xcontent.XContentBuilder;
-import org.elasticsearch.xpack.core.XPackBuild;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-public class XPackInfoResponse extends ActionResponse {
-
-    @Nullable private BuildInfo buildInfo;
-    @Nullable private LicenseInfo licenseInfo;
-    @Nullable private FeatureSetsInfo featureSetsInfo;
-
-    public XPackInfoResponse() {}
-
-    public XPackInfoResponse(@Nullable BuildInfo buildInfo, @Nullable LicenseInfo licenseInfo, @Nullable FeatureSetsInfo featureSetsInfo) {
-        this.buildInfo = buildInfo;
-        this.licenseInfo = licenseInfo;
-        this.featureSetsInfo = featureSetsInfo;
-    }
-
-    /**
-     * @return  The build info (incl. build hash and timestamp)
-     */
-    public BuildInfo getBuildInfo() {
-        return buildInfo;
-    }
-
-    /**
-     * @return  The current license info (incl. UID, type/mode. status and expiry date). May return {@code null} when no
-     *          license is currently installed.
-     */
-    public LicenseInfo getLicenseInfo() {
-        return licenseInfo;
-    }
-
-    /**
-     * @return  The current status of the feature sets in X-Pack. Feature sets describe the features available/enabled in X-Pack.
-     */
-    public FeatureSetsInfo getFeatureSetsInfo() {
-        return featureSetsInfo;
-    }
-
-    @Override
-    public void writeTo(StreamOutput out) throws IOException {
-        super.writeTo(out);
-        out.writeOptionalWriteable(buildInfo);
-        out.writeOptionalWriteable(licenseInfo);
-        out.writeOptionalWriteable(featureSetsInfo);
-    }
-
-    @Override
-    public void readFrom(StreamInput in) throws IOException {
-        this.buildInfo = in.readOptionalWriteable(BuildInfo::new);
-        this.licenseInfo = in.readOptionalWriteable(LicenseInfo::new);
-        this.featureSetsInfo = in.readOptionalWriteable(FeatureSetsInfo::new);
-    }
-
-    public static class LicenseInfo implements ToXContentObject, Writeable {
-
-        private final String uid;
-        private final String type;
-        private final String mode;
-        private final long expiryDate;
-        private final License.Status status;
-
-        public LicenseInfo(License license) {
-            this(license.uid(), license.type(), license.operationMode().name().toLowerCase(Locale.ROOT),
-                    license.status(), license.expiryDate());
-        }
-
-        public LicenseInfo(StreamInput in) throws IOException {
-            this(in.readString(), in.readString(), in.readString(), License.Status.readFrom(in), in.readLong());
-        }
-
-        public LicenseInfo(String uid, String type, String mode, License.Status status, long expiryDate) {
-            this.uid = uid;
-            this.type = type;
-            this.mode = mode;
-            this.status = status;
-            this.expiryDate = expiryDate;
-        }
-
-        public String getUid() {
-            return uid;
-        }
-
-        public String getType() {
-            return type;
-        }
-
-        public String getMode() {
-            return mode;
-        }
-
-        public long getExpiryDate() {
-            return expiryDate;
-        }
-
-        public License.Status getStatus() {
-            return status;
-        }
-
-        @Override
-        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-            builder.startObject()
-                .field("uid", uid)
-                .field("type", type)
-                .field("mode", mode)
-                .field("status", status.label());
-            if (expiryDate != LicenseService.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS) {
-                builder.timeField("expiry_date_in_millis", "expiry_date", expiryDate);
-            }
-            return builder.endObject();
-        }
-
-        public void writeTo(StreamOutput out) throws IOException {
-            out.writeString(uid);
-            out.writeString(type);
-            out.writeString(mode);
-            status.writeTo(out);
-            out.writeLong(expiryDate);
-        }
-    }
-
-    public static class BuildInfo implements ToXContentObject, Writeable {
-
-        private final String hash;
-        private final String timestamp;
-
-        public BuildInfo(XPackBuild build) {
-            this(build.shortHash(), build.date());
-        }
-
-        public BuildInfo(StreamInput input) throws IOException {
-            this(input.readString(), input.readString());
-        }
-
-        public BuildInfo(String hash, String timestamp) {
-            this.hash = hash;
-            this.timestamp = timestamp;
-        }
-
-        public String getHash() {
-            return hash;
-        }
-
-        public String getTimestamp() {
-            return timestamp;
-        }
-
-        @Override
-        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-            return builder.startObject()
-                    .field("hash", hash)
-                    .field("date", timestamp)
-                    .endObject();
-        }
-
-        public void writeTo(StreamOutput output) throws IOException {
-            output.writeString(hash);
-            output.writeString(timestamp);
-        }
-    }
-
-    public static class FeatureSetsInfo implements ToXContentObject, Writeable {
-
-        private final Map<String, FeatureSet> featureSets;
-
-        public FeatureSetsInfo(StreamInput in) throws IOException {
-            int size = in.readVInt();
-            Map<String, FeatureSet> featureSets = new HashMap<>(size);
-            for (int i = 0; i < size; i++) {
-                FeatureSet featureSet = new FeatureSet(in);
-                featureSets.put(featureSet.name, featureSet);
-            }
-            this.featureSets = Collections.unmodifiableMap(featureSets);
-        }
-
-        public FeatureSetsInfo(Set<FeatureSet> featureSets) {
-            Map<String, FeatureSet> map = new HashMap<>(featureSets.size());
-            for (FeatureSet featureSet : featureSets) {
-                map.put(featureSet.name, featureSet);
-            }
-            this.featureSets = Collections.unmodifiableMap(map);
-        }
-
-        public Map<String, FeatureSet> getFeatureSets() {
-            return featureSets;
-        }
-
-        @Override
-        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-            builder.startObject();
-            List<String> names = new ArrayList<>(this.featureSets.keySet()).stream().sorted().collect(Collectors.toList());
-            for (String name : names) {
-                builder.field(name, featureSets.get(name), params);
-            }
-            return builder.endObject();
-        }
-
-        public void writeTo(StreamOutput out) throws IOException {
-            out.writeVInt(featureSets.size());
-            for (FeatureSet featureSet : featureSets.values()) {
-                featureSet.writeTo(out);
-            }
-        }
-
-        public static class FeatureSet implements ToXContentObject, Writeable {
-
-            private final String name;
-            @Nullable private final String description;
-            private final boolean available;
-            private final boolean enabled;
-            @Nullable private final Map<String, Object> nativeCodeInfo;
-
-            public FeatureSet(StreamInput in) throws IOException {
-                this(in.readString(), in.readOptionalString(), in.readBoolean(), in.readBoolean(),
-                        in.getVersion().onOrAfter(Version.V_5_4_0) ? in.readMap() : null);
-            }
-
-            public FeatureSet(String name, @Nullable String description, boolean available, boolean enabled,
-                              @Nullable Map<String, Object> nativeCodeInfo) {
-                this.name = name;
-                this.description = description;
-                this.available = available;
-                this.enabled = enabled;
-                this.nativeCodeInfo = nativeCodeInfo;
-            }
-
-            public String name() {
-                return name;
-            }
-
-            @Nullable
-            public String description() {
-                return description;
-            }
-
-            public boolean available() {
-                return available;
-            }
-
-            public boolean enabled() {
-                return enabled;
-            }
-
-            @Nullable
-            public Map<String, Object> nativeCodeInfo() {
-                return nativeCodeInfo;
-            }
-
-            public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
-                builder.startObject();
-                if (description != null) {
-                    builder.field("description", description);
-                }
-                builder.field("available", available);
-                builder.field("enabled", enabled);
-                if (nativeCodeInfo != null) {
-                    builder.field("native_code_info", nativeCodeInfo);
-                }
-                return builder.endObject();
-            }
-
-            public void writeTo(StreamOutput out) throws IOException {
-                out.writeString(name);
-                out.writeOptionalString(description);
-                out.writeBoolean(available);
-                out.writeBoolean(enabled);
-                if (out.getVersion().onOrAfter(Version.V_5_4_0)) {
-                    out.writeMap(nativeCodeInfo);
-                }
-            }
-        }
-
-    }
-}

+ 2 - 2
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClient.java

@@ -9,9 +9,9 @@ import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.client.Client;
 import org.elasticsearch.common.settings.SecureString;
 import org.elasticsearch.license.LicensingClient;
-import org.elasticsearch.license.XPackInfoResponse;
+import org.elasticsearch.protocol.xpack.XPackInfoRequest;
+import org.elasticsearch.protocol.xpack.XPackInfoResponse;
 import org.elasticsearch.xpack.core.action.XPackInfoAction;
-import org.elasticsearch.xpack.core.action.XPackInfoRequest;
 import org.elasticsearch.xpack.core.action.XPackInfoRequestBuilder;
 import org.elasticsearch.xpack.core.ml.client.MachineLearningClient;
 import org.elasticsearch.xpack.core.monitoring.client.MonitoringClient;

+ 8 - 5
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/action/TransportXPackInfoAction.java

@@ -12,14 +12,16 @@ import org.elasticsearch.common.inject.Inject;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.license.License;
 import org.elasticsearch.license.LicenseService;
-import org.elasticsearch.license.XPackInfoResponse;
-import org.elasticsearch.license.XPackInfoResponse.FeatureSetsInfo.FeatureSet;
-import org.elasticsearch.license.XPackInfoResponse.LicenseInfo;
+import org.elasticsearch.protocol.xpack.XPackInfoRequest;
+import org.elasticsearch.protocol.xpack.XPackInfoResponse;
+import org.elasticsearch.protocol.xpack.XPackInfoResponse.FeatureSetsInfo.FeatureSet;
+import org.elasticsearch.protocol.xpack.XPackInfoResponse.LicenseInfo;
 import org.elasticsearch.tasks.Task;
 import org.elasticsearch.transport.TransportService;
 import org.elasticsearch.xpack.core.XPackBuild;
 import org.elasticsearch.xpack.core.XPackFeatureSet;
 
+import java.util.Locale;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -43,14 +45,15 @@ public class TransportXPackInfoAction extends HandledTransportAction<XPackInfoRe
 
         XPackInfoResponse.BuildInfo buildInfo = null;
         if (request.getCategories().contains(XPackInfoRequest.Category.BUILD)) {
-            buildInfo = new XPackInfoResponse.BuildInfo(XPackBuild.CURRENT);
+            buildInfo = new XPackInfoResponse.BuildInfo(XPackBuild.CURRENT.shortHash(), XPackBuild.CURRENT.date());
         }
 
         LicenseInfo licenseInfo = null;
         if (request.getCategories().contains(XPackInfoRequest.Category.LICENSE)) {
             License license = licenseService.getLicense();
             if (license != null) {
-                licenseInfo = new LicenseInfo(license);
+                licenseInfo = new LicenseInfo(license.uid(), license.type(), license.operationMode().name().toLowerCase(Locale.ROOT),
+                        license.status(), license.expiryDate());
             }
         }
 

+ 1 - 1
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/action/XPackInfoAction.java

@@ -6,7 +6,7 @@
 package org.elasticsearch.xpack.core.action;
 
 import org.elasticsearch.action.Action;
-import org.elasticsearch.license.XPackInfoResponse;
+import org.elasticsearch.protocol.xpack.XPackInfoResponse;
 
 public class XPackInfoAction extends Action<XPackInfoResponse> {
 

+ 2 - 1
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/action/XPackInfoRequestBuilder.java

@@ -7,7 +7,8 @@ package org.elasticsearch.xpack.core.action;
 
 import org.elasticsearch.action.ActionRequestBuilder;
 import org.elasticsearch.client.ElasticsearchClient;
-import org.elasticsearch.license.XPackInfoResponse;
+import org.elasticsearch.protocol.xpack.XPackInfoRequest;
+import org.elasticsearch.protocol.xpack.XPackInfoResponse;
 
 import java.util.EnumSet;
 

+ 3 - 38
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/rest/action/RestXPackInfoAction.java

@@ -6,15 +6,11 @@
 package org.elasticsearch.xpack.core.rest.action;
 
 import org.elasticsearch.common.settings.Settings;
-import org.elasticsearch.common.xcontent.XContentBuilder;
-import org.elasticsearch.license.XPackInfoResponse;
-import org.elasticsearch.rest.BytesRestResponse;
+import org.elasticsearch.protocol.xpack.XPackInfoRequest;
 import org.elasticsearch.rest.RestController;
 import org.elasticsearch.rest.RestRequest;
-import org.elasticsearch.rest.RestResponse;
-import org.elasticsearch.rest.action.RestBuilderListener;
+import org.elasticsearch.rest.action.RestToXContentListener;
 import org.elasticsearch.xpack.core.XPackClient;
-import org.elasticsearch.xpack.core.action.XPackInfoRequest;
 import org.elasticsearch.xpack.core.rest.XPackRestHandler;
 
 import java.io.IOException;
@@ -22,7 +18,6 @@ import java.util.EnumSet;
 
 import static org.elasticsearch.rest.RestRequest.Method.GET;
 import static org.elasticsearch.rest.RestRequest.Method.HEAD;
-import static org.elasticsearch.rest.RestStatus.OK;
 
 public class RestXPackInfoAction extends XPackRestHandler {
     public RestXPackInfoAction(Settings settings, RestController controller) {
@@ -48,36 +43,6 @@ public class RestXPackInfoAction extends XPackRestHandler {
                 client.prepareInfo()
                         .setVerbose(verbose)
                         .setCategories(categories)
-                        .execute(new RestBuilderListener<XPackInfoResponse>(channel) {
-                            @Override
-                            public RestResponse buildResponse(XPackInfoResponse infoResponse, XContentBuilder builder) throws Exception {
-
-                                builder.startObject();
-
-                                if (infoResponse.getBuildInfo() != null) {
-                                    builder.field("build", infoResponse.getBuildInfo(), request);
-                                }
-
-                                if (infoResponse.getLicenseInfo() != null) {
-                                    builder.field("license", infoResponse.getLicenseInfo(), request);
-                                } else if (categories.contains(XPackInfoRequest.Category.LICENSE)) {
-                                    // if the user requested the license info, and there is no license, we should send
-                                    // back an explicit null value (indicating there is no license). This is different
-                                    // than not adding the license info at all
-                                    builder.nullField("license");
-                                }
-
-                                if (infoResponse.getFeatureSetsInfo() != null) {
-                                    builder.field("features", infoResponse.getFeatureSetsInfo(), request);
-                                }
-
-                                if (verbose) {
-                                    builder.field("tagline", "You know, for X");
-                                }
-
-                                builder.endObject();
-                                return new BytesRestResponse(OK, builder);
-                            }
-                        });
+                        .execute(new RestToXContentListener<>(channel));
     }
 }

+ 5 - 3
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/action/TransportXPackInfoActionTests.java

@@ -10,8 +10,10 @@ import org.elasticsearch.action.support.ActionFilters;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.license.License;
 import org.elasticsearch.license.LicenseService;
-import org.elasticsearch.license.XPackInfoResponse;
-import org.elasticsearch.license.XPackInfoResponse.FeatureSetsInfo.FeatureSet;
+import org.elasticsearch.protocol.license.LicenseStatus;
+import org.elasticsearch.protocol.xpack.XPackInfoRequest;
+import org.elasticsearch.protocol.xpack.XPackInfoResponse;
+import org.elasticsearch.protocol.xpack.XPackInfoResponse.FeatureSetsInfo.FeatureSet;
 import org.elasticsearch.tasks.Task;
 import org.elasticsearch.test.ESTestCase;
 import org.elasticsearch.transport.Transport;
@@ -61,7 +63,7 @@ public class TransportXPackInfoActionTests extends ESTestCase {
         License license = mock(License.class);
         long expiryDate = randomLong();
         when(license.expiryDate()).thenReturn(expiryDate);
-        License.Status status = randomFrom(License.Status.values());
+        LicenseStatus status = randomFrom(LicenseStatus.values());
         when(license.status()).thenReturn(status);
         String type = randomAlphaOfLength(10);
         when(license.type()).thenReturn(type);

+ 5 - 4
x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/datafeed/MlRemoteLicenseChecker.java

@@ -13,11 +13,12 @@ import org.elasticsearch.common.Nullable;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.util.concurrent.ThreadContext;
 import org.elasticsearch.license.License;
-import org.elasticsearch.license.XPackInfoResponse;
+import org.elasticsearch.protocol.license.LicenseStatus;
+import org.elasticsearch.protocol.xpack.XPackInfoRequest;
+import org.elasticsearch.protocol.xpack.XPackInfoResponse;
 import org.elasticsearch.transport.ActionNotFoundTransportException;
 import org.elasticsearch.transport.RemoteClusterAware;
 import org.elasticsearch.xpack.core.action.XPackInfoAction;
-import org.elasticsearch.xpack.core.action.XPackInfoRequest;
 
 import java.util.EnumSet;
 import java.util.Iterator;
@@ -136,7 +137,7 @@ public class MlRemoteLicenseChecker {
 
     static boolean licenseSupportsML(XPackInfoResponse.LicenseInfo licenseInfo) {
         License.OperationMode mode = License.OperationMode.resolve(licenseInfo.getMode());
-        return licenseInfo.getStatus() == License.Status.ACTIVE &&
+        return licenseInfo.getStatus() == LicenseStatus.ACTIVE &&
                 (mode == License.OperationMode.PLATINUM || mode == License.OperationMode.TRIAL);
     }
 
@@ -173,7 +174,7 @@ public class MlRemoteLicenseChecker {
 
     public static String buildErrorMessage(RemoteClusterLicenseInfo clusterLicenseInfo) {
         StringBuilder error = new StringBuilder();
-        if (clusterLicenseInfo.licenseInfo.getStatus() != License.Status.ACTIVE) {
+        if (clusterLicenseInfo.licenseInfo.getStatus() != LicenseStatus.ACTIVE) {
             error.append("The license on cluster [").append(clusterLicenseInfo.clusterName)
                     .append("] is not active. ");
         } else {

+ 9 - 9
x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/datafeed/MlRemoteLicenseCheckerTests.java

@@ -11,8 +11,8 @@ import org.elasticsearch.client.Client;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.util.concurrent.ThreadContext;
-import org.elasticsearch.license.License;
-import org.elasticsearch.license.XPackInfoResponse;
+import org.elasticsearch.protocol.license.LicenseStatus;
+import org.elasticsearch.protocol.xpack.XPackInfoResponse;
 import org.elasticsearch.test.ESTestCase;
 import org.elasticsearch.threadpool.ThreadPool;
 import org.elasticsearch.xpack.core.action.XPackInfoAction;
@@ -66,16 +66,16 @@ public class MlRemoteLicenseCheckerTests extends ESTestCase {
 
     public void testLicenseSupportsML() {
         XPackInfoResponse.LicenseInfo licenseInfo = new XPackInfoResponse.LicenseInfo("uid", "trial", "trial",
-                License.Status.ACTIVE, randomNonNegativeLong());
+                LicenseStatus.ACTIVE, randomNonNegativeLong());
         assertTrue(MlRemoteLicenseChecker.licenseSupportsML(licenseInfo));
 
-        licenseInfo = new XPackInfoResponse.LicenseInfo("uid", "trial", "trial", License.Status.EXPIRED, randomNonNegativeLong());
+        licenseInfo = new XPackInfoResponse.LicenseInfo("uid", "trial", "trial", LicenseStatus.EXPIRED, randomNonNegativeLong());
         assertFalse(MlRemoteLicenseChecker.licenseSupportsML(licenseInfo));
 
-        licenseInfo = new XPackInfoResponse.LicenseInfo("uid", "GOLD", "GOLD", License.Status.ACTIVE, randomNonNegativeLong());
+        licenseInfo = new XPackInfoResponse.LicenseInfo("uid", "GOLD", "GOLD", LicenseStatus.ACTIVE, randomNonNegativeLong());
         assertFalse(MlRemoteLicenseChecker.licenseSupportsML(licenseInfo));
 
-        licenseInfo = new XPackInfoResponse.LicenseInfo("uid", "PLATINUM", "PLATINUM", License.Status.ACTIVE, randomNonNegativeLong());
+        licenseInfo = new XPackInfoResponse.LicenseInfo("uid", "PLATINUM", "PLATINUM", LicenseStatus.ACTIVE, randomNonNegativeLong());
         assertTrue(MlRemoteLicenseChecker.licenseSupportsML(licenseInfo));
     }
 
@@ -186,14 +186,14 @@ public class MlRemoteLicenseCheckerTests extends ESTestCase {
     }
 
     private XPackInfoResponse.LicenseInfo createPlatinumLicenseResponse() {
-        return new XPackInfoResponse.LicenseInfo("uid", "PLATINUM", "PLATINUM", License.Status.ACTIVE, randomNonNegativeLong());
+        return new XPackInfoResponse.LicenseInfo("uid", "PLATINUM", "PLATINUM", LicenseStatus.ACTIVE, randomNonNegativeLong());
     }
 
     private XPackInfoResponse.LicenseInfo createBasicLicenseResponse() {
-        return new XPackInfoResponse.LicenseInfo("uid", "BASIC", "BASIC", License.Status.ACTIVE, randomNonNegativeLong());
+        return new XPackInfoResponse.LicenseInfo("uid", "BASIC", "BASIC", LicenseStatus.ACTIVE, randomNonNegativeLong());
     }
 
     private XPackInfoResponse.LicenseInfo createExpiredLicenseResponse() {
-        return new XPackInfoResponse.LicenseInfo("uid", "PLATINUM", "PLATINUM", License.Status.EXPIRED, randomNonNegativeLong());
+        return new XPackInfoResponse.LicenseInfo("uid", "PLATINUM", "PLATINUM", LicenseStatus.EXPIRED, randomNonNegativeLong());
     }
 }

+ 3 - 3
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/SetupPasswordToolTests.java

@@ -27,9 +27,9 @@ import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.common.xcontent.json.JsonXContent;
 import org.elasticsearch.env.Environment;
 import org.elasticsearch.env.TestEnvironment;
-import org.elasticsearch.license.XPackInfoResponse;
-import org.elasticsearch.license.XPackInfoResponse.FeatureSetsInfo;
-import org.elasticsearch.license.XPackInfoResponse.FeatureSetsInfo.FeatureSet;
+import org.elasticsearch.protocol.xpack.XPackInfoResponse;
+import org.elasticsearch.protocol.xpack.XPackInfoResponse.FeatureSetsInfo;
+import org.elasticsearch.protocol.xpack.XPackInfoResponse.FeatureSetsInfo.FeatureSet;
 import org.elasticsearch.rest.RestStatus;
 import org.elasticsearch.xpack.core.security.support.Validation;
 import org.elasticsearch.xpack.core.security.user.ElasticUser;

+ 202 - 0
x-pack/protocol/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.

+ 29 - 0
x-pack/protocol/build.gradle

@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+apply plugin: 'elasticsearch.build'
+
+description = 'Request and Response objects for x-pack that are used by the' +
+        ' high level rest client and x-pack itself'
+
+dependencies {
+    compileOnly "org.elasticsearch:elasticsearch:${version}"
+
+    testCompile "org.elasticsearch.test:framework:${version}"
+}

+ 67 - 0
x-pack/protocol/src/main/java/org/elasticsearch/protocol/license/LicenseStatus.java

@@ -0,0 +1,67 @@
+/*
+ * 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.protocol.license;
+
+import java.io.IOException;
+
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+
+/**
+ * Status of an X-Pack license.
+ */
+public enum LicenseStatus implements Writeable {
+
+    ACTIVE("active"),
+    INVALID("invalid"),
+    EXPIRED("expired");
+
+    private final String label;
+
+    LicenseStatus(String label) {
+        this.label = label;
+    }
+
+    public String label() {
+        return label;
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        out.writeString(label);
+    }
+
+    public static LicenseStatus readFrom(StreamInput in) throws IOException {
+        return fromString(in.readString());
+    }
+
+    public static LicenseStatus fromString(String value) {
+        switch (value) {
+            case "active":
+                return ACTIVE;
+            case "invalid":
+                return INVALID;
+            case "expired":
+                return EXPIRED;
+            default:
+                throw new IllegalArgumentException("unknown license status [" + value + "]");
+        }
+    }
+}

+ 24 - 0
x-pack/protocol/src/main/java/org/elasticsearch/protocol/license/package-info.java

@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+/**
+ * Request and Response objects for the default distribution's License
+ * APIs.
+ */
+package org.elasticsearch.protocol.license;

+ 24 - 0
x-pack/protocol/src/main/java/org/elasticsearch/protocol/security/package-info.java

@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+/**
+ * Request and Response objects for the default distribution's Security
+ * APIs.
+ */
+package org.elasticsearch.protocol.security;

+ 24 - 0
x-pack/protocol/src/main/java/org/elasticsearch/protocol/watcher/package-info.java

@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+/**
+ * Request and Response objects for the default distribution's Watcher
+ * APIs.
+ */
+package org.elasticsearch.protocol.watcher;

+ 20 - 4
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/action/XPackInfoRequest.java → x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/XPackInfoRequest.java

@@ -1,9 +1,22 @@
 /*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
+ * 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.xpack.core.action;
+package org.elasticsearch.protocol.xpack;
 
 import org.elasticsearch.action.ActionRequest;
 import org.elasticsearch.action.ActionRequestValidationException;
@@ -14,6 +27,9 @@ import java.io.IOException;
 import java.util.EnumSet;
 import java.util.Locale;
 
+/**
+ * Fetch information about X-Pack from the cluster.
+ */
 public class XPackInfoRequest extends ActionRequest {
 
     public enum Category {

+ 500 - 0
x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/XPackInfoResponse.java

@@ -0,0 +1,500 @@
+/*
+ * 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.protocol.xpack;
+
+import org.elasticsearch.Version;
+import org.elasticsearch.action.ActionResponse;
+import org.elasticsearch.common.Nullable;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.ObjectParser.ValueType;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.protocol.license.LicenseStatus;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
+
+public class XPackInfoResponse extends ActionResponse implements ToXContentObject {
+    /**
+     * Value of the license's expiration time if it should never expire.
+     */
+    public static final long BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS = Long.MAX_VALUE - TimeUnit.HOURS.toMillis(24 * 365);
+    // TODO move this constant to License.java once we move License.java to the protocol jar
+
+    @Nullable private BuildInfo buildInfo;
+    @Nullable private LicenseInfo licenseInfo;
+    @Nullable private FeatureSetsInfo featureSetsInfo;
+
+    public XPackInfoResponse() {}
+
+    public XPackInfoResponse(@Nullable BuildInfo buildInfo, @Nullable LicenseInfo licenseInfo, @Nullable FeatureSetsInfo featureSetsInfo) {
+        this.buildInfo = buildInfo;
+        this.licenseInfo = licenseInfo;
+        this.featureSetsInfo = featureSetsInfo;
+    }
+
+    /**
+     * @return  The build info (incl. build hash and timestamp)
+     */
+    public BuildInfo getBuildInfo() {
+        return buildInfo;
+    }
+
+    /**
+     * @return  The current license info (incl. UID, type/mode. status and expiry date). May return {@code null} when no
+     *          license is currently installed.
+     */
+    public LicenseInfo getLicenseInfo() {
+        return licenseInfo;
+    }
+
+    /**
+     * @return  The current status of the feature sets in X-Pack. Feature sets describe the features available/enabled in X-Pack.
+     */
+    public FeatureSetsInfo getFeatureSetsInfo() {
+        return featureSetsInfo;
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        super.writeTo(out);
+        out.writeOptionalWriteable(buildInfo);
+        out.writeOptionalWriteable(licenseInfo);
+        out.writeOptionalWriteable(featureSetsInfo);
+    }
+
+    @Override
+    public void readFrom(StreamInput in) throws IOException {
+        this.buildInfo = in.readOptionalWriteable(BuildInfo::new);
+        this.licenseInfo = in.readOptionalWriteable(LicenseInfo::new);
+        this.featureSetsInfo = in.readOptionalWriteable(FeatureSetsInfo::new);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == null || other.getClass() != getClass()) return false;
+        if (this == other) return true;
+        XPackInfoResponse rhs = (XPackInfoResponse) other;
+        return Objects.equals(buildInfo, rhs.buildInfo)
+                && Objects.equals(licenseInfo, rhs.licenseInfo)
+                && Objects.equals(featureSetsInfo, rhs.featureSetsInfo);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(buildInfo, licenseInfo, featureSetsInfo);
+    }
+
+    @Override
+    public String toString() {
+        return Strings.toString(this, true, false);
+    }
+
+    private static final ConstructingObjectParser<XPackInfoResponse, Void> PARSER = new ConstructingObjectParser<>(
+            "xpack_info_response", true, (a, v) -> {
+                BuildInfo buildInfo = (BuildInfo) a[0];
+                LicenseInfo licenseInfo = (LicenseInfo) a[1];
+                @SuppressWarnings("unchecked") // This is how constructing object parser works
+                List<FeatureSetsInfo.FeatureSet> featureSets = (List<FeatureSetsInfo.FeatureSet>) a[2];
+                FeatureSetsInfo featureSetsInfo = featureSets == null ? null : new FeatureSetsInfo(new HashSet<>(featureSets));
+                return new XPackInfoResponse(buildInfo, licenseInfo, featureSetsInfo);
+            });
+    static {
+        PARSER.declareObject(optionalConstructorArg(), BuildInfo.PARSER, new ParseField("build"));
+        /*
+         * licenseInfo is sort of "double optional" because it is
+         * optional but it can also be send as `null`.
+         */
+        PARSER.declareField(optionalConstructorArg(), (p, v) -> {
+                    if (p.currentToken() == XContentParser.Token.VALUE_NULL) {
+                        return null;
+                    }
+                    return LicenseInfo.PARSER.parse(p, v);
+                },
+                new ParseField("license"), ValueType.OBJECT_OR_NULL);
+        PARSER.declareNamedObjects(optionalConstructorArg(),
+                (p, c, name) -> FeatureSetsInfo.FeatureSet.PARSER.parse(p, name),
+                new ParseField("features"));
+    }
+    public static XPackInfoResponse fromXContent(XContentParser parser) throws IOException {
+        return PARSER.parse(parser, null);
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        builder.startObject();
+
+        if (buildInfo != null) {
+            builder.field("build", buildInfo, params);
+        }
+
+        EnumSet<XPackInfoRequest.Category> categories = XPackInfoRequest.Category
+                .toSet(Strings.splitStringByCommaToArray(params.param("categories", "_all")));
+        if (licenseInfo != null) {
+            builder.field("license", licenseInfo, params);
+        } else if (categories.contains(XPackInfoRequest.Category.LICENSE)) {
+            // if the user requested the license info, and there is no license, we should send
+            // back an explicit null value (indicating there is no license). This is different
+            // than not adding the license info at all
+            builder.nullField("license");
+        }
+
+        if (featureSetsInfo != null) {
+            builder.field("features", featureSetsInfo, params);
+        }
+
+        if (params.paramAsBoolean("human", true)) {
+            builder.field("tagline", "You know, for X");
+        }
+
+        return builder.endObject();
+    }
+
+    public static class LicenseInfo implements ToXContentObject, Writeable {
+        private final String uid;
+        private final String type;
+        private final String mode;
+        private final LicenseStatus status;
+        private final long expiryDate;
+
+        public LicenseInfo(String uid, String type, String mode, LicenseStatus status, long expiryDate) {
+            this.uid = uid;
+            this.type = type;
+            this.mode = mode;
+            this.status = status;
+            this.expiryDate = expiryDate;
+        }
+
+        public LicenseInfo(StreamInput in) throws IOException {
+            this(in.readString(), in.readString(), in.readString(), LicenseStatus.readFrom(in), in.readLong());
+        }
+
+        @Override
+        public void writeTo(StreamOutput out) throws IOException {
+            out.writeString(uid);
+            out.writeString(type);
+            out.writeString(mode);
+            status.writeTo(out);
+            out.writeLong(expiryDate);
+        }
+
+        public String getUid() {
+            return uid;
+        }
+
+        public String getType() {
+            return type;
+        }
+
+        public String getMode() {
+            return mode;
+        }
+
+        public long getExpiryDate() {
+            return expiryDate;
+        }
+
+        public LicenseStatus getStatus() {
+            return status;
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (other == null || other.getClass() != getClass()) return false;
+            if (this == other) return true;
+            LicenseInfo rhs = (LicenseInfo) other;
+            return Objects.equals(uid, rhs.uid)
+                    && Objects.equals(type, rhs.type)
+                    && Objects.equals(mode, rhs.mode)
+                    && Objects.equals(status, rhs.status)
+                    && expiryDate == rhs.expiryDate;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(uid, type, mode, status, expiryDate);
+        }
+
+        private static final ConstructingObjectParser<LicenseInfo, Void> PARSER = new ConstructingObjectParser<>(
+                "license_info", true, (a, v) -> {
+                    String uid = (String) a[0];
+                    String type = (String) a[1];
+                    String mode = (String) a[2];
+                    LicenseStatus status = LicenseStatus.fromString((String) a[3]);
+                    Long expiryDate = (Long) a[4];
+                    long primitiveExpiryDate = expiryDate == null ? BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS : expiryDate;
+                    return new LicenseInfo(uid, type, mode, status, primitiveExpiryDate);
+                });
+        static {
+            PARSER.declareString(constructorArg(), new ParseField("uid"));
+            PARSER.declareString(constructorArg(), new ParseField("type"));
+            PARSER.declareString(constructorArg(), new ParseField("mode"));
+            PARSER.declareString(constructorArg(), new ParseField("status"));
+            PARSER.declareLong(optionalConstructorArg(), new ParseField("expiry_date_in_millis"));
+        }
+
+        @Override
+        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+            builder.startObject()
+                .field("uid", uid)
+                .field("type", type)
+                .field("mode", mode)
+                .field("status", status.label());
+            if (expiryDate != BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS) {
+                builder.timeField("expiry_date_in_millis", "expiry_date", expiryDate);
+            }
+            return builder.endObject();
+        }
+    }
+
+    public static class BuildInfo implements ToXContentObject, Writeable {
+        private final String hash;
+        private final String timestamp;
+
+        public BuildInfo(String hash, String timestamp) {
+            this.hash = hash;
+            this.timestamp = timestamp;
+        }
+
+        public BuildInfo(StreamInput input) throws IOException {
+            this(input.readString(), input.readString());
+        }
+
+        @Override
+        public void writeTo(StreamOutput output) throws IOException {
+            output.writeString(hash);
+            output.writeString(timestamp);
+        }
+
+        public String getHash() {
+            return hash;
+        }
+
+        public String getTimestamp() {
+            return timestamp;
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (other == null || other.getClass() != getClass()) return false;
+            if (this == other) return true;
+            BuildInfo rhs = (BuildInfo) other;
+            return Objects.equals(hash, rhs.hash)
+                    && Objects.equals(timestamp, rhs.timestamp);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(hash, timestamp);
+        }
+
+        private static final ConstructingObjectParser<BuildInfo, Void> PARSER = new ConstructingObjectParser<>(
+                "build_info", true, (a, v) -> new BuildInfo((String) a[0], (String) a[1]));
+        static {
+            PARSER.declareString(constructorArg(), new ParseField("hash"));
+            PARSER.declareString(constructorArg(), new ParseField("date"));
+        }
+
+        @Override
+        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+            return builder.startObject()
+                    .field("hash", hash)
+                    .field("date", timestamp)
+                    .endObject();
+        }
+    }
+
+    public static class FeatureSetsInfo implements ToXContentObject, Writeable {
+        private final Map<String, FeatureSet> featureSets;
+
+        public FeatureSetsInfo(Set<FeatureSet> featureSets) {
+            Map<String, FeatureSet> map = new HashMap<>(featureSets.size());
+            for (FeatureSet featureSet : featureSets) {
+                map.put(featureSet.name, featureSet);
+            }
+            this.featureSets = Collections.unmodifiableMap(map);
+        }
+
+        public FeatureSetsInfo(StreamInput in) throws IOException {
+            int size = in.readVInt();
+            Map<String, FeatureSet> featureSets = new HashMap<>(size);
+            for (int i = 0; i < size; i++) {
+                FeatureSet featureSet = new FeatureSet(in);
+                featureSets.put(featureSet.name, featureSet);
+            }
+            this.featureSets = Collections.unmodifiableMap(featureSets);
+        }
+
+        @Override
+        public void writeTo(StreamOutput out) throws IOException {
+            out.writeVInt(featureSets.size());
+            for (FeatureSet featureSet : featureSets.values()) {
+                featureSet.writeTo(out);
+            }
+        }
+
+        public Map<String, FeatureSet> getFeatureSets() {
+            return featureSets;
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (other == null || other.getClass() != getClass()) return false;
+            if (this == other) return true;
+            FeatureSetsInfo rhs = (FeatureSetsInfo) other;
+            return Objects.equals(featureSets, rhs.featureSets);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(featureSets);
+        }
+
+        @Override
+        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+            builder.startObject();
+            List<String> names = new ArrayList<>(this.featureSets.keySet()).stream().sorted().collect(Collectors.toList());
+            for (String name : names) {
+                builder.field(name, featureSets.get(name), params);
+            }
+            return builder.endObject();
+        }
+
+        public static class FeatureSet implements ToXContentObject, Writeable {
+            private final String name;
+            @Nullable private final String description;
+            private final boolean available;
+            private final boolean enabled;
+            @Nullable private final Map<String, Object> nativeCodeInfo;
+
+            public FeatureSet(String name, @Nullable String description, boolean available, boolean enabled,
+                              @Nullable Map<String, Object> nativeCodeInfo) {
+                this.name = name;
+                this.description = description;
+                this.available = available;
+                this.enabled = enabled;
+                this.nativeCodeInfo = nativeCodeInfo;
+            }
+
+            public FeatureSet(StreamInput in) throws IOException {
+                this(in.readString(), in.readOptionalString(), in.readBoolean(), in.readBoolean(),
+                        in.getVersion().onOrAfter(Version.V_5_4_0) ? in.readMap() : null);
+            }
+
+            @Override
+            public void writeTo(StreamOutput out) throws IOException {
+                out.writeString(name);
+                out.writeOptionalString(description);
+                out.writeBoolean(available);
+                out.writeBoolean(enabled);
+                if (out.getVersion().onOrAfter(Version.V_5_4_0)) {
+                    out.writeMap(nativeCodeInfo);
+                }
+            }
+
+            public String name() {
+                return name;
+            }
+
+            @Nullable
+            public String description() {
+                return description;
+            }
+
+            public boolean available() {
+                return available;
+            }
+
+            public boolean enabled() {
+                return enabled;
+            }
+
+            @Nullable
+            public Map<String, Object> nativeCodeInfo() {
+                return nativeCodeInfo;
+            }
+
+            @Override
+            public boolean equals(Object other) {
+                if (other == null || other.getClass() != getClass()) return false;
+                if (this == other) return true;
+                FeatureSet rhs = (FeatureSet) other;
+                return Objects.equals(name, rhs.name)
+                        && Objects.equals(description, rhs.description)
+                        && available == rhs.available
+                        && enabled == rhs.enabled
+                        && Objects.equals(nativeCodeInfo, rhs.nativeCodeInfo);
+            }
+
+            @Override
+            public int hashCode() {
+                return Objects.hash(name, description, available, enabled, nativeCodeInfo);
+            }
+
+            private static final ConstructingObjectParser<FeatureSet, String> PARSER = new ConstructingObjectParser<>(
+                    "feature_set", true, (a, name) -> {
+                        String description = (String) a[0];
+                        boolean available = (Boolean) a[1];
+                        boolean enabled = (Boolean) a[2];
+                        @SuppressWarnings("unchecked") // Matches up with declaration below
+                        Map<String, Object> nativeCodeInfo = (Map<String, Object>) a[3];
+                        return new FeatureSet(name, description, available, enabled, nativeCodeInfo);
+                    });
+            static {
+                PARSER.declareString(optionalConstructorArg(), new ParseField("description"));
+                PARSER.declareBoolean(constructorArg(), new ParseField("available"));
+                PARSER.declareBoolean(constructorArg(), new ParseField("enabled"));
+                PARSER.declareObject(optionalConstructorArg(), (p, name) -> p.map(), new ParseField("native_code_info"));
+            }
+
+            @Override
+            public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+                builder.startObject();
+                if (description != null) {
+                    builder.field("description", description);
+                }
+                builder.field("available", available);
+                builder.field("enabled", enabled);
+                if (nativeCodeInfo != null) {
+                    builder.field("native_code_info", nativeCodeInfo);
+                }
+                return builder.endObject();
+            }
+        }
+    }
+}

+ 23 - 0
x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/package-info.java

@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+/**
+ * Request and Response objects for miscellaneous X-Pack APIs.
+ */
+package org.elasticsearch.protocol.xpack;

+ 30 - 0
x-pack/protocol/src/test/java/org/elasticsearch/protocol/license/LicenseStatusTests.java

@@ -0,0 +1,30 @@
+/*
+ * 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.protocol.license;
+
+import java.io.IOException;
+
+import org.elasticsearch.test.ESTestCase;
+
+public class LicenseStatusTests extends ESTestCase {
+    public void testSerialization() throws IOException {
+        LicenseStatus status = randomFrom(LicenseStatus.values());
+        assertSame(status, copyWriteable(status, writableRegistry(), LicenseStatus::readFrom));
+    }
+}

+ 160 - 0
x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/XPackInfoResponseTests.java

@@ -0,0 +1,160 @@
+/*
+ * 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.protocol.xpack;
+
+import org.elasticsearch.common.xcontent.ToXContent;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.protocol.license.LicenseStatus;
+import org.elasticsearch.protocol.xpack.XPackInfoResponse.BuildInfo;
+import org.elasticsearch.protocol.xpack.XPackInfoResponse.LicenseInfo;
+import org.elasticsearch.protocol.xpack.XPackInfoResponse.FeatureSetsInfo;
+import org.elasticsearch.protocol.xpack.XPackInfoResponse.FeatureSetsInfo.FeatureSet;
+import org.elasticsearch.test.AbstractStreamableXContentTestCase;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.io.IOException;
+
+public class XPackInfoResponseTests extends AbstractStreamableXContentTestCase<XPackInfoResponse> {
+    @Override
+    protected XPackInfoResponse doParseInstance(XContentParser parser) throws IOException {
+        return XPackInfoResponse.fromXContent(parser);
+    }
+
+    @Override
+    protected XPackInfoResponse createBlankInstance() {
+        return new XPackInfoResponse();
+    }
+
+    @Override
+    protected Predicate<String> getRandomFieldsExcludeFilter() {
+        return path -> path.equals("features")
+                || (path.startsWith("features") && path.endsWith("native_code_info"));
+    }
+
+    @Override
+    protected ToXContent.Params getToXContentParams() {
+        Map<String, String> params = new HashMap<>();
+        if (randomBoolean()) {
+            params.put("human", randomBoolean() ? "true" : "false");
+        }
+        if (randomBoolean()) {
+            params.put("categories", "_none");
+        }
+        return new ToXContent.MapParams(params);
+    }
+
+    @Override
+    protected XPackInfoResponse createTestInstance() {
+        return new XPackInfoResponse(
+            randomBoolean() ? null : randomBuildInfo(),
+            randomBoolean() ? null : randomLicenseInfo(),
+            randomBoolean() ? null : randomFeatureSetsInfo());
+    }
+
+    @Override
+    protected XPackInfoResponse mutateInstance(XPackInfoResponse response) {
+        @SuppressWarnings("unchecked")
+        Function<XPackInfoResponse, XPackInfoResponse> mutator = randomFrom(
+            r -> new XPackInfoResponse(
+                    mutateBuildInfo(r.getBuildInfo()),
+                    r.getLicenseInfo(),
+                    r.getFeatureSetsInfo()),
+            r -> new XPackInfoResponse(
+                    r.getBuildInfo(),
+                    mutateLicenseInfo(r.getLicenseInfo()),
+                    r.getFeatureSetsInfo()),
+            r -> new XPackInfoResponse(
+                    r.getBuildInfo(),
+                    r.getLicenseInfo(),
+                    mutateFeatureSetsInfo(r.getFeatureSetsInfo())));
+        return mutator.apply(response);
+    }
+
+    private BuildInfo randomBuildInfo() {
+        return new BuildInfo(
+            randomAlphaOfLength(10),
+            randomAlphaOfLength(15));
+    }
+
+    private BuildInfo mutateBuildInfo(BuildInfo buildInfo) {
+        if (buildInfo == null) {
+            return randomBuildInfo();
+        }
+        return null;
+    }
+
+    private LicenseInfo randomLicenseInfo() {
+        return new LicenseInfo(
+            randomAlphaOfLength(10),
+            randomAlphaOfLength(4),
+            randomAlphaOfLength(5),
+            randomFrom(LicenseStatus.values()),
+            randomLong());
+    }
+
+    private LicenseInfo mutateLicenseInfo(LicenseInfo licenseInfo) {
+        if (licenseInfo == null) {
+            return randomLicenseInfo();
+        }
+        return null;
+    }
+
+    private FeatureSetsInfo randomFeatureSetsInfo() {
+        int size = between(0, 10);
+        Set<FeatureSet> featureSets = new HashSet<>(size);
+        while (featureSets.size() < size) {
+            featureSets.add(randomFeatureSet());
+        }
+        return new FeatureSetsInfo(featureSets);
+    }
+
+    private FeatureSetsInfo mutateFeatureSetsInfo(FeatureSetsInfo featureSetsInfo) {
+        if (featureSetsInfo == null) {
+            return randomFeatureSetsInfo();
+        }
+        return null;
+    }
+
+    private FeatureSet randomFeatureSet() {
+        return new FeatureSet(
+            randomAlphaOfLength(5),
+            randomBoolean() ? null : randomAlphaOfLength(20),
+            randomBoolean(),
+            randomBoolean(),
+            randomNativeCodeInfo());
+    }
+
+    private Map<String, Object> randomNativeCodeInfo() {
+        if (randomBoolean()) {
+            return null;
+        }
+        int size = between(0, 10);
+        Map<String, Object> nativeCodeInfo = new HashMap<>(size);
+        while (nativeCodeInfo.size() < size) {
+            nativeCodeInfo.put(randomAlphaOfLength(5), randomAlphaOfLength(5));
+        }
+        return nativeCodeInfo;
+    }
+}