diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/LicenseClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/LicenseClient.java
new file mode 100644
index 0000000000000..587578f3b35e1
--- /dev/null
+++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/LicenseClient.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client;
+
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.protocol.xpack.license.PutLicenseRequest;
+import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
+
+import java.io.IOException;
+
+import static java.util.Collections.emptySet;
+
+/**
+ * A wrapper for the {@link RestHighLevelClient} that provides methods for
+ * accessing the Elastic License-related methods
+ *
+ * See the
+ * X-Pack Licensing APIs on elastic.co for more information.
+ */
+public class LicenseClient {
+
+ private final RestHighLevelClient restHighLevelClient;
+
+ LicenseClient(RestHighLevelClient restHighLevelClient) {
+ this.restHighLevelClient = restHighLevelClient;
+ }
+
+ /**
+ * Updates license for the cluster.
+ * @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 PutLicenseResponse putLicense(PutLicenseRequest request, RequestOptions options) throws IOException {
+ return restHighLevelClient.performRequestAndParseEntity(request, RequestConverters::putLicense, options,
+ PutLicenseResponse::fromXContent, emptySet());
+ }
+
+ /**
+ * Asynchronously updates license for the cluster cluster.
+ * @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 putLicenseAsync(PutLicenseRequest request, RequestOptions options, ActionListener listener) {
+ restHighLevelClient.performRequestAsyncAndParseEntity(request, RequestConverters::putLicense, options,
+ PutLicenseResponse::fromXContent, listener, emptySet());
+ }
+
+}
diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java
index a6122b0681e91..5ad507136018d 100644
--- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java
+++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java
@@ -108,6 +108,7 @@
import org.elasticsearch.protocol.xpack.XPackInfoRequest;
import org.elasticsearch.protocol.xpack.watcher.PutWatchRequest;
import org.elasticsearch.protocol.xpack.XPackUsageRequest;
+import org.elasticsearch.protocol.xpack.license.PutLicenseRequest;
import org.elasticsearch.rest.action.search.RestSearchAction;
import org.elasticsearch.script.mustache.MultiSearchTemplateRequest;
import org.elasticsearch.script.mustache.SearchTemplateRequest;
@@ -1124,6 +1125,18 @@ static Request xpackUsage(XPackUsageRequest usageRequest) {
return request;
}
+ static Request putLicense(PutLicenseRequest putLicenseRequest) {
+ Request request = new Request(HttpPut.METHOD_NAME, "/_xpack/license");
+ Params parameters = new Params(request);
+ parameters.withTimeout(putLicenseRequest.timeout());
+ parameters.withMasterTimeout(putLicenseRequest.masterNodeTimeout());
+ if (putLicenseRequest.isAcknowledge()) {
+ parameters.putParam("acknowledge", "true");
+ }
+ request.setJsonEntity(putLicenseRequest.getLicenseDefinition());
+ 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));
diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/XPackClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/XPackClient.java
index 4acaadfdb85d5..1401376527df2 100644
--- a/client/rest-high-level/src/main/java/org/elasticsearch/client/XPackClient.java
+++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/XPackClient.java
@@ -42,10 +42,12 @@ public final class XPackClient {
private final RestHighLevelClient restHighLevelClient;
private final WatcherClient watcherClient;
+ private final LicenseClient licenseClient;
XPackClient(RestHighLevelClient restHighLevelClient) {
this.restHighLevelClient = restHighLevelClient;
this.watcherClient = new WatcherClient(restHighLevelClient);
+ this.licenseClient = new LicenseClient(restHighLevelClient);
}
public WatcherClient watcher() {
@@ -100,4 +102,15 @@ public void usageAsync(XPackUsageRequest request, RequestOptions options, Action
restHighLevelClient.performRequestAsyncAndParseEntity(request, RequestConverters::xpackUsage, options,
XPackUsageResponse::fromXContent, listener, emptySet());
}
+
+ /**
+ * A wrapper for the {@link RestHighLevelClient} that provides methods for
+ * accessing the Elastic Licensing APIs.
+ *
+ * See the
+ * X-Pack APIs on elastic.co for more information.
+ */
+ public LicenseClient license() {
+ return licenseClient;
+ }
}
diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/LicensingDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/LicensingDocumentationIT.java
new file mode 100644
index 0000000000000..2563c0462ba2a
--- /dev/null
+++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/LicensingDocumentationIT.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client.documentation;
+
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.LatchedActionListener;
+import org.elasticsearch.client.ESRestHighLevelClientTestCase;
+import org.elasticsearch.client.RequestOptions;
+import org.elasticsearch.client.RestHighLevelClient;
+import org.elasticsearch.protocol.xpack.license.LicensesStatus;
+import org.elasticsearch.protocol.xpack.license.PutLicenseRequest;
+import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
+
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.startsWith;
+
+/**
+ * Documentation for Licensing APIs in the high level java client.
+ * Code wrapped in {@code tag} and {@code end} tags is included in the docs.
+ */
+public class LicensingDocumentationIT extends ESRestHighLevelClientTestCase {
+
+ public void testPutLicense() throws Exception {
+ RestHighLevelClient client = highLevelClient();
+ String license = "{\"license\": {\"uid\":\"893361dc-9749-4997-93cb-802e3d7fa4a8\",\"type\":\"gold\"," +
+ "\"issue_date_in_millis\":1411948800000,\"expiry_date_in_millis\":1914278399999,\"max_nodes\":1,\"issued_to\":\"issued_to\"," +
+ "\"issuer\":\"issuer\",\"signature\":\"AAAAAgAAAA3U8+YmnvwC+CWsV/mRAAABmC9ZN0hjZDBGYnVyRXpCOW5Bb3FjZDAxOWpSbTVoMVZwUzRxVk1PSm" +
+ "kxakxZdW5IMlhlTHNoN1N2MXMvRFk4d3JTZEx3R3RRZ0pzU3lobWJKZnQvSEFva0ppTHBkWkprZWZSQi9iNmRQNkw1SlpLN0lDalZCS095MXRGN1lIZlpYcVVTTn" +
+ "FrcTE2dzhJZmZrdFQrN3JQeGwxb0U0MXZ0dDJHSERiZTVLOHNzSDByWnpoZEphZHBEZjUrTVBxRENNSXNsWWJjZllaODdzVmEzUjNiWktNWGM5TUhQV2plaUo4Q1" +
+ "JOUml4MXNuL0pSOEhQaVB2azhmUk9QVzhFeTFoM1Q0RnJXSG53MWk2K055c28zSmRnVkF1b2JSQkFLV2VXUmVHNDZ2R3o2VE1qbVNQS2lxOHN5bUErZlNIWkZSVm" +
+ "ZIWEtaSU9wTTJENDVvT1NCYklacUYyK2FwRW9xa0t6dldMbmMzSGtQc3FWOTgzZ3ZUcXMvQkt2RUZwMFJnZzlvL2d2bDRWUzh6UG5pdENGWFRreXNKNkE9PQAAAQ" +
+ "Be8GfzDm6T537Iuuvjetb3xK5dvg0K5NQapv+rczWcQFxgCuzbF8plkgetP1aAGZP4uRESDQPMlOCsx4d0UqqAm9f7GbBQ3l93P+PogInPFeEH9NvOmaAQovmxVM" +
+ "9SE6DsDqlX4cXSO+bgWpXPTd2LmpoQc1fXd6BZ8GeuyYpVHVKp9hVU0tAYjw6HzYOE7+zuO1oJYOxElqy66AnIfkvHrvni+flym3tE7tDTgsDRaz7W3iBhaqiSnt" +
+ "EqabEkvHdPHQdSR99XGaEvnHO1paK01/35iZF6OXHsF7CCj+558GRXiVxzueOe7TsGSSt8g7YjZwV9bRCyU7oB4B/nidgI\"}}";
+ {
+ //tag::put-license-execute
+ PutLicenseRequest request = new PutLicenseRequest();
+ request.setLicenseDefinition(license); // <1>
+ request.setAcknowledge(false); // <2>
+
+ PutLicenseResponse response = client.xpack().license().putLicense(request, RequestOptions.DEFAULT);
+ //end::put-license-execute
+
+ //tag::put-license-response
+ LicensesStatus status = response.status(); // <1>
+ assertEquals(status, LicensesStatus.VALID); // <2>
+ boolean acknowledged = response.isAcknowledged(); // <3>
+ String acknowledgeHeader = response.acknowledgeHeader(); // <4>
+ Map acknowledgeMessages = response.acknowledgeMessages(); // <5>
+ //end::put-license-response
+
+ assertFalse(acknowledged); // Should fail because we are trying to downgrade from platinum trial to gold
+ assertThat(acknowledgeHeader, startsWith("This license update requires acknowledgement."));
+ assertThat(acknowledgeMessages.keySet(), not(hasSize(0)));
+ }
+ {
+ PutLicenseRequest request = new PutLicenseRequest();
+ // tag::put-license-execute-listener
+ ActionListener listener = new ActionListener() {
+ @Override
+ public void onResponse(PutLicenseResponse indexResponse) {
+ // <1>
+ }
+
+ @Override
+ public void onFailure(Exception e) {
+ // <2>
+ }
+ };
+ // end::put-license-execute-listener
+
+ // Replace the empty listener by a blocking listener in test
+ final CountDownLatch latch = new CountDownLatch(1);
+ listener = new LatchedActionListener<>(listener, latch);
+
+ // tag::put-license-execute-async
+ client.xpack().license().putLicenseAsync(
+ request, RequestOptions.DEFAULT, listener); // <1>
+ // end::put-license-execute-async
+
+ assertTrue(latch.await(30L, TimeUnit.SECONDS));
+ }
+ }
+}
diff --git a/docs/java-rest/high-level/licensing/put-license.asciidoc b/docs/java-rest/high-level/licensing/put-license.asciidoc
new file mode 100644
index 0000000000000..d467600d6ac19
--- /dev/null
+++ b/docs/java-rest/high-level/licensing/put-license.asciidoc
@@ -0,0 +1,64 @@
+[[java-rest-high-put-license]]
+=== Update License
+
+[[java-rest-high-put-license-execution]]
+==== Execution
+
+The license can be added or updated using the `putLicense()` method:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/LicensingDocumentationIT.java[put-license-execute]
+--------------------------------------------------
+<1> 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.
+<2> A JSON document containing the license information.
+
+[[java-rest-high-put-license-response]]
+==== Response
+
+The returned `PutLicenseResponse` contains the `LicensesStatus`,
+`acknowledged` flag and possible acknowledge messages. The acknowledge messages
+are present if you previously had a license with more features than one you
+are trying to update and you didn't set the `acknowledge` flag to `true`. In this case
+you need to display the messages to the end user and if they agree, resubmit the
+license with the `acknowledge` flag set to `true`.
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/LicensingDocumentationIT.java[put-license-response]
+--------------------------------------------------
+<1> The status of the license
+<2> Make sure that the license is valid.
+<3> Check the acknowledge flag.
+<4> It should be true if license is acknowledge.
+<5> Otherwise we can see the acknowledge messages in `acknowledgeHeader()`
+<6> and check component-specific messages in `acknowledgeMessages()`.
+
+[[java-rest-high-put-license-async]]
+==== Asynchronous Execution
+
+This request can be executed asynchronously:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/LicensingDocumentationIT.java[put-license-execute-async]
+--------------------------------------------------
+<1> The `PutLicenseRequest` 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 `PutLicenseResponse` looks like:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/LicensingDocumentationIT.java[put-license-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
diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc
index d952870677b7e..25fbcaaaeaa73 100644
--- a/docs/java-rest/high-level/supported-apis.asciidoc
+++ b/docs/java-rest/high-level/supported-apis.asciidoc
@@ -186,3 +186,12 @@ The Java High Level REST Client supports the following Scripts APIs:
include::script/get_script.asciidoc[]
include::script/delete_script.asciidoc[]
+
+
+== Licensing APIs
+
+The Java High Level REST Client supports the following Licensing APIs:
+
+* <>
+
+include::licensing/put-license.asciidoc[]
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java
index a39e9f412d767..42e59deae3817 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java
@@ -28,6 +28,8 @@
import org.elasticsearch.env.Environment;
import org.elasticsearch.gateway.GatewayService;
import org.elasticsearch.protocol.xpack.XPackInfoResponse;
+import org.elasticsearch.protocol.xpack.license.LicensesStatus;
+import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
import org.elasticsearch.watcher.ResourceWatcherService;
import org.elasticsearch.xpack.core.XPackPlugin;
import org.elasticsearch.xpack.core.XPackSettings;
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicensesStatus.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicensesStatus.java
deleted file mode 100644
index 91e0d7239cfa1..0000000000000
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicensesStatus.java
+++ /dev/null
@@ -1,34 +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;
-
-public enum LicensesStatus {
- VALID((byte) 0),
- INVALID((byte) 1),
- EXPIRED((byte) 2);
-
- private final byte id;
-
- LicensesStatus(byte id) {
- this.id = id;
- }
-
- public int id() {
- return id;
- }
-
- public static LicensesStatus fromId(int id) {
- if (id == 0) {
- return VALID;
- } else if (id == 1) {
- return INVALID;
- } else if (id == 2) {
- return EXPIRED;
- } else {
- throw new IllegalStateException("no valid LicensesStatus for id=" + id);
- }
- }
-}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicensingClient.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicensingClient.java
index 07979d15ea5ef..14a059e9e014a 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicensingClient.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicensingClient.java
@@ -7,6 +7,7 @@
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.client.ElasticsearchClient;
+import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
public class LicensingClient {
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/PutLicenseAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/PutLicenseAction.java
index d93957a9d8bae..497b203f4136d 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/PutLicenseAction.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/PutLicenseAction.java
@@ -6,6 +6,7 @@
package org.elasticsearch.license;
import org.elasticsearch.action.Action;
+import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
public class PutLicenseAction extends Action {
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/PutLicenseRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/PutLicenseRequestBuilder.java
index b7c93d03cd5ff..bc5201a8f1ffd 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/PutLicenseRequestBuilder.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/PutLicenseRequestBuilder.java
@@ -9,6 +9,7 @@
import org.elasticsearch.client.ElasticsearchClient;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
/**
* Register license request builder
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/PutLicenseResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/PutLicenseResponse.java
deleted file mode 100644
index c85bb068da39e..0000000000000
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/PutLicenseResponse.java
+++ /dev/null
@@ -1,119 +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.action.support.master.AcknowledgedResponse;
-import org.elasticsearch.common.Strings;
-import org.elasticsearch.common.io.stream.StreamInput;
-import org.elasticsearch.common.io.stream.StreamOutput;
-import org.elasticsearch.common.xcontent.XContentBuilder;
-
-import java.io.IOException;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-
-public class PutLicenseResponse extends AcknowledgedResponse {
-
- private LicensesStatus status;
- private Map acknowledgeMessages;
- private String acknowledgeHeader;
-
- PutLicenseResponse() {
- }
-
- public PutLicenseResponse(boolean acknowledged, LicensesStatus status) {
- this(acknowledged, status, null, Collections.emptyMap());
- }
-
- public PutLicenseResponse(boolean acknowledged, LicensesStatus status, String acknowledgeHeader,
- Map acknowledgeMessages) {
- super(acknowledged);
- this.status = status;
- this.acknowledgeHeader = acknowledgeHeader;
- this.acknowledgeMessages = acknowledgeMessages;
- }
-
- public LicensesStatus status() {
- return status;
- }
-
- public Map acknowledgeMessages() {
- return acknowledgeMessages;
- }
-
- public String acknowledgeHeader() {
- return acknowledgeHeader;
- }
-
- @Override
- public void readFrom(StreamInput in) throws IOException {
- super.readFrom(in);
- status = LicensesStatus.fromId(in.readVInt());
- acknowledgeHeader = in.readOptionalString();
- int size = in.readVInt();
- Map acknowledgeMessages = new HashMap<>(size);
- for (int i = 0; i < size; i++) {
- String feature = in.readString();
- int nMessages = in.readVInt();
- String[] messages = new String[nMessages];
- for (int j = 0; j < nMessages; j++) {
- messages[j] = in.readString();
- }
- acknowledgeMessages.put(feature, messages);
- }
- this.acknowledgeMessages = acknowledgeMessages;
- }
-
- @Override
- public void writeTo(StreamOutput out) throws IOException {
- super.writeTo(out);
- out.writeVInt(status.id());
- out.writeOptionalString(acknowledgeHeader);
- out.writeVInt(acknowledgeMessages.size());
- for (Map.Entry entry : acknowledgeMessages.entrySet()) {
- out.writeString(entry.getKey());
- out.writeVInt(entry.getValue().length);
- for (String message : entry.getValue()) {
- out.writeString(message);
- }
- }
- }
-
- @Override
- protected void addCustomFields(XContentBuilder builder, Params params) throws IOException {
- switch (status) {
- case VALID:
- builder.field("license_status", "valid");
- break;
- case INVALID:
- builder.field("license_status", "invalid");
- break;
- case EXPIRED:
- builder.field("license_status", "expired");
- break;
- default:
- throw new IllegalArgumentException("unknown status [" + status + "] found");
- }
- if (!acknowledgeMessages.isEmpty()) {
- builder.startObject("acknowledge");
- builder.field("message", acknowledgeHeader);
- for (Map.Entry entry : acknowledgeMessages.entrySet()) {
- builder.startArray(entry.getKey());
- for (String message : entry.getValue()) {
- builder.value(message);
- }
- builder.endArray();
- }
- builder.endObject();
- }
- }
-
- @Override
- public String toString() {
- return Strings.toString(this, true, true);
- }
-}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/TransportPutLicenseAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/TransportPutLicenseAction.java
index 032d1eb6e86d1..571bfc1413c58 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/TransportPutLicenseAction.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/TransportPutLicenseAction.java
@@ -16,6 +16,7 @@
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.TransportService;
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseTLSTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseTLSTests.java
index 588dbabb9db8a..754b398cd6c9d 100644
--- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseTLSTests.java
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseTLSTests.java
@@ -12,6 +12,7 @@
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.TransportAddress;
import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
import java.net.InetAddress;
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicensesAcknowledgementTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicensesAcknowledgementTests.java
index 8e1c363c814da..06afd2f7ca382 100644
--- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicensesAcknowledgementTests.java
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicensesAcknowledgementTests.java
@@ -9,6 +9,8 @@
import org.elasticsearch.cluster.ClusterStateUpdateTask;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.protocol.xpack.license.LicensesStatus;
+import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
import static org.elasticsearch.common.unit.TimeValue.timeValueHours;
import static org.hamcrest.Matchers.equalTo;
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicensesManagerServiceTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicensesManagerServiceTests.java
index 540dbd891bd9b..c397bd79e2885 100644
--- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicensesManagerServiceTests.java
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicensesManagerServiceTests.java
@@ -11,6 +11,7 @@
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.plugins.Plugin;
+import org.elasticsearch.protocol.xpack.license.LicensesStatus;
import org.elasticsearch.test.ESSingleNodeTestCase;
import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin;
import org.elasticsearch.xpack.core.XPackSettings;
@@ -145,4 +146,4 @@ public void onFailure(Exception throwable) {
}
assertThat("remove license(s) failed", success.get(), equalTo(true));
}
-}
\ No newline at end of file
+}
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicensesTransportTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicensesTransportTests.java
index a48132ef3d79d..aa372eb03562a 100644
--- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicensesTransportTests.java
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicensesTransportTests.java
@@ -12,6 +12,8 @@
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.node.Node;
import org.elasticsearch.plugins.Plugin;
+import org.elasticsearch.protocol.xpack.license.LicensesStatus;
+import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
import org.elasticsearch.test.ESSingleNodeTestCase;
import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin;
import org.elasticsearch.xpack.core.XPackSettings;
@@ -230,4 +232,4 @@ public void testLicenseIsAcceptedWhenStartDateBeforeThanNow() throws Exception {
assertThat(putLicenseResponse.isAcknowledged(), equalTo(true));
assertThat(putLicenseResponse.status(), equalTo(LicensesStatus.VALID));
}
-}
\ No newline at end of file
+}
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/PutLicenseResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/PutLicenseResponseTests.java
deleted file mode 100644
index d4b7900fa5bc8..0000000000000
--- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/PutLicenseResponseTests.java
+++ /dev/null
@@ -1,103 +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.common.bytes.BytesReference;
-import org.elasticsearch.common.io.stream.BytesStreamOutput;
-import org.elasticsearch.common.io.stream.StreamInput;
-import org.elasticsearch.common.xcontent.ToXContent;
-import org.elasticsearch.common.xcontent.XContentBuilder;
-import org.elasticsearch.common.xcontent.XContentFactory;
-import org.elasticsearch.common.xcontent.XContentHelper;
-import org.elasticsearch.test.ESTestCase;
-
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-
-import static org.hamcrest.Matchers.equalTo;
-import static org.hamcrest.Matchers.not;
-import static org.hamcrest.Matchers.sameInstance;
-
-public class PutLicenseResponseTests extends ESTestCase {
- @SuppressWarnings("unchecked")
- public void testSerialization() throws Exception {
- boolean acknowledged = randomBoolean();
- LicensesStatus status = randomFrom(LicensesStatus.VALID, LicensesStatus.INVALID, LicensesStatus.EXPIRED);
- Map ackMessages = randomAckMessages();
-
- PutLicenseResponse response = new PutLicenseResponse(acknowledged, status, "", ackMessages);
-
- XContentBuilder contentBuilder = XContentFactory.jsonBuilder();
- response.toXContent(contentBuilder, ToXContent.EMPTY_PARAMS);
-
- Map map = XContentHelper.convertToMap(BytesReference.bytes(contentBuilder), false,
- contentBuilder.contentType()).v2();
- assertThat(map.containsKey("acknowledged"), equalTo(true));
- boolean actualAcknowledged = (boolean) map.get("acknowledged");
- assertThat(actualAcknowledged, equalTo(acknowledged));
-
- assertThat(map.containsKey("license_status"), equalTo(true));
- String actualStatus = (String) map.get("license_status");
- assertThat(actualStatus, equalTo(status.name().toLowerCase(Locale.ROOT)));
-
- assertThat(map.containsKey("acknowledge"), equalTo(true));
- Map> actualAckMessages = (Map>) map.get("acknowledge");
- assertTrue(actualAckMessages.containsKey("message"));
- actualAckMessages.remove("message");
- assertThat(actualAckMessages.keySet(), equalTo(ackMessages.keySet()));
- for (Map.Entry> entry : actualAckMessages.entrySet()) {
- assertArrayEquals(entry.getValue().toArray(), ackMessages.get(entry.getKey()));
- }
- }
-
- public void testStreamSerialization() throws IOException {
- boolean acknowledged = randomBoolean();
- LicensesStatus status = randomFrom(LicensesStatus.VALID, LicensesStatus.INVALID, LicensesStatus.EXPIRED);
- Map ackMessages = randomAckMessages();
-
- // write the steam so that we can attempt to read it back
- BytesStreamOutput output = new BytesStreamOutput();
-
- PutLicenseResponse response = new PutLicenseResponse(acknowledged, status, "", ackMessages);
- // write it out
- response.writeTo(output);
-
- StreamInput input = output.bytes().streamInput();
-
- // read it back in
- response.readFrom(input);
-
- assertThat(response.isAcknowledged(), equalTo(acknowledged));
- assertThat(response.status(), equalTo(status));
- assertThat(response.acknowledgeMessages(), not(sameInstance(ackMessages)));
- assertThat(response.acknowledgeMessages().size(), equalTo(ackMessages.size()));
-
- for (String key : ackMessages.keySet()) {
- assertArrayEquals(ackMessages.get(key), response.acknowledgeMessages().get(key));
- }
- }
-
- private static Map randomAckMessages() {
- int nFeatures = randomIntBetween(1, 5);
-
- Map ackMessages = new HashMap<>();
-
- for (int i = 0; i < nFeatures; i++) {
- String feature = randomAlphaOfLengthBetween(9, 15);
- int nMessages = randomIntBetween(1, 5);
- String[] messages = new String[nMessages];
- for (int j = 0; j < nMessages; j++) {
- messages[j] = randomAlphaOfLengthBetween(10, 30);
- }
- ackMessages.put(feature, messages);
- }
-
- return ackMessages;
- }
-}
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/TestUtils.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/TestUtils.java
index d236dacaa4d99..1cb89fa8a06f2 100644
--- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/TestUtils.java
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/TestUtils.java
@@ -19,6 +19,8 @@
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.license.licensor.LicenseSigner;
+import org.elasticsearch.protocol.xpack.license.LicensesStatus;
+import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
import org.hamcrest.MatcherAssert;
import org.joda.time.format.DateTimeFormatter;
import org.junit.Assert;
diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/common/ProtocolUtils.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/common/ProtocolUtils.java
new file mode 100644
index 0000000000000..e135cdc50e926
--- /dev/null
+++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/common/ProtocolUtils.java
@@ -0,0 +1,72 @@
+/*
+ * 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.common;
+
+import java.util.Arrays;
+import java.util.Map;
+
+/**
+ * Common utilities used for XPack protocol classes
+ */
+public final class ProtocolUtils {
+
+ /**
+ * Implements equals for a map of string arrays
+ *
+ * The map of string arrays is used in some XPack protocol classes but does't work with equal.
+ */
+ public static boolean equals(Map a, Map b) {
+ if (a == null) {
+ return b == null;
+ }
+ if (b == null) {
+ return false;
+ }
+ if (a.size() != b.size()) {
+ return false;
+ }
+ for (Map.Entry entry : a.entrySet()) {
+ String[] val = entry.getValue();
+ String key = entry.getKey();
+ if (val == null) {
+ if (b.get(key) != null || b.containsKey(key) == false) {
+ return false;
+ }
+ } else {
+ if (Arrays.equals(val, b.get(key)) == false) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Implements hashCode for map of string arrays
+ *
+ * The map of string arrays does't work with hashCode.
+ */
+ public static int hashCode(Map a) {
+ int hash = 0;
+ for (Map.Entry entry : a.entrySet())
+ hash += Arrays.hashCode(entry.getValue());
+ return hash;
+ }
+}
diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/license/LicensesStatus.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/license/LicensesStatus.java
new file mode 100644
index 0000000000000..2a3ed924fbe4b
--- /dev/null
+++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/license/LicensesStatus.java
@@ -0,0 +1,68 @@
+/*
+ * 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.license;
+
+import java.util.Locale;
+
+public enum LicensesStatus {
+ VALID((byte) 0),
+ INVALID((byte) 1),
+ EXPIRED((byte) 2);
+
+ private final byte id;
+
+ LicensesStatus(byte id) {
+ this.id = id;
+ }
+
+ public int id() {
+ return id;
+ }
+
+ public static LicensesStatus fromId(int id) {
+ if (id == 0) {
+ return VALID;
+ } else if (id == 1) {
+ return INVALID;
+ } else if (id == 2) {
+ return EXPIRED;
+ } else {
+ throw new IllegalStateException("no valid LicensesStatus for id=" + id);
+ }
+ }
+
+
+ @Override
+ public String toString() {
+ return this.name().toLowerCase(Locale.ROOT);
+ }
+
+ public static LicensesStatus fromString(String value) {
+ switch (value) {
+ case "valid":
+ return VALID;
+ case "invalid":
+ return INVALID;
+ case "expired":
+ return EXPIRED;
+ default:
+ throw new IllegalArgumentException("unknown licenses status [" + value + "]");
+ }
+ }
+}
diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/license/PutLicenseRequest.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/license/PutLicenseRequest.java
new file mode 100644
index 0000000000000..97101a3ccd483
--- /dev/null
+++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/license/PutLicenseRequest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.license;
+
+import org.elasticsearch.action.ActionRequestValidationException;
+import org.elasticsearch.action.support.master.AcknowledgedRequest;
+
+public class PutLicenseRequest extends AcknowledgedRequest {
+
+ private String licenseDefinition;
+ private boolean acknowledge = false;
+
+ public PutLicenseRequest() {
+
+ }
+
+ @Override
+ public ActionRequestValidationException validate() {
+ return null;
+ }
+
+ public void setLicenseDefinition(String licenseDefinition) {
+ this.licenseDefinition = licenseDefinition;
+ }
+
+ public String getLicenseDefinition() {
+ return licenseDefinition;
+ }
+
+ public void setAcknowledge(boolean acknowledge) {
+ this.acknowledge = acknowledge;
+ }
+
+ public boolean isAcknowledge() {
+ return acknowledge;
+ }
+}
diff --git a/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/license/PutLicenseResponse.java b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/license/PutLicenseResponse.java
new file mode 100644
index 0000000000000..9c4ff51d92a10
--- /dev/null
+++ b/x-pack/protocol/src/main/java/org/elasticsearch/protocol/xpack/license/PutLicenseResponse.java
@@ -0,0 +1,209 @@
+/*
+ * 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.license;
+
+import org.elasticsearch.action.support.master.AcknowledgedResponse;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.collect.Tuple;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParseException;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.protocol.xpack.common.ProtocolUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
+
+public class PutLicenseResponse extends AcknowledgedResponse {
+
+ private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(
+ "put_license_response", true, (a, v) -> {
+ boolean acknowledged = (Boolean) a[0];
+ LicensesStatus licensesStatus = LicensesStatus.fromString((String) a[1]);
+ @SuppressWarnings("unchecked") Tuple> acknowledgements = (Tuple>) a[2];
+ if (acknowledgements == null) {
+ return new PutLicenseResponse(acknowledged, licensesStatus);
+ } else {
+ return new PutLicenseResponse(acknowledged, licensesStatus, acknowledgements.v1(), acknowledgements.v2());
+ }
+
+ });
+
+ static {
+ PARSER.declareBoolean(constructorArg(), new ParseField("acknowledged"));
+ PARSER.declareString(constructorArg(), new ParseField("license_status"));
+ PARSER.declareObject(optionalConstructorArg(), (parser, v) -> {
+ Map acknowledgeMessages = new HashMap<>();
+ String message = null;
+ XContentParser.Token token;
+ String currentFieldName = null;
+ while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
+ if (token == XContentParser.Token.FIELD_NAME) {
+ currentFieldName = parser.currentName();
+ } else {
+ if (currentFieldName == null) {
+ throw new XContentParseException(parser.getTokenLocation(), "expected message header or acknowledgement");
+ }
+ if ("message".equals(currentFieldName)) {
+ if (token != XContentParser.Token.VALUE_STRING) {
+ throw new XContentParseException(parser.getTokenLocation(), "unexpected message header type");
+ }
+ message = parser.text();
+ } else {
+ if (token != XContentParser.Token.START_ARRAY) {
+ throw new XContentParseException(parser.getTokenLocation(), "unexpected acknowledgement type");
+ }
+ List acknowledgeMessagesList = new ArrayList<>();
+ while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
+ if (token != XContentParser.Token.VALUE_STRING) {
+ throw new XContentParseException(parser.getTokenLocation(), "unexpected acknowledgement text");
+ }
+ acknowledgeMessagesList.add(parser.text());
+ }
+ acknowledgeMessages.put(currentFieldName, acknowledgeMessagesList.toArray(new String[0]));
+ }
+ }
+ }
+ return new Tuple<>(message, acknowledgeMessages);
+ },
+ new ParseField("acknowledge"));
+ }
+
+ private LicensesStatus status;
+ private Map acknowledgeMessages;
+ private String acknowledgeHeader;
+
+ public PutLicenseResponse() {
+ }
+
+ public PutLicenseResponse(boolean acknowledged, LicensesStatus status) {
+ this(acknowledged, status, null, Collections.emptyMap());
+ }
+
+ public PutLicenseResponse(boolean acknowledged, LicensesStatus status, String acknowledgeHeader,
+ Map acknowledgeMessages) {
+ super(acknowledged);
+ this.status = status;
+ this.acknowledgeHeader = acknowledgeHeader;
+ this.acknowledgeMessages = acknowledgeMessages;
+ }
+
+ public LicensesStatus status() {
+ return status;
+ }
+
+ public Map acknowledgeMessages() {
+ return acknowledgeMessages;
+ }
+
+ public String acknowledgeHeader() {
+ return acknowledgeHeader;
+ }
+
+ @Override
+ public void readFrom(StreamInput in) throws IOException {
+ super.readFrom(in);
+ status = LicensesStatus.fromId(in.readVInt());
+ acknowledgeHeader = in.readOptionalString();
+ int size = in.readVInt();
+ Map acknowledgeMessages = new HashMap<>(size);
+ for (int i = 0; i < size; i++) {
+ String feature = in.readString();
+ int nMessages = in.readVInt();
+ String[] messages = new String[nMessages];
+ for (int j = 0; j < nMessages; j++) {
+ messages[j] = in.readString();
+ }
+ acknowledgeMessages.put(feature, messages);
+ }
+ this.acknowledgeMessages = acknowledgeMessages;
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ super.writeTo(out);
+ out.writeVInt(status.id());
+ out.writeOptionalString(acknowledgeHeader);
+ out.writeVInt(acknowledgeMessages.size());
+ for (Map.Entry entry : acknowledgeMessages.entrySet()) {
+ out.writeString(entry.getKey());
+ out.writeVInt(entry.getValue().length);
+ for (String message : entry.getValue()) {
+ out.writeString(message);
+ }
+ }
+ }
+
+ @Override
+ protected void addCustomFields(XContentBuilder builder, Params params) throws IOException {
+ builder.field("license_status", status.toString());
+ if (!acknowledgeMessages.isEmpty()) {
+ builder.startObject("acknowledge");
+ builder.field("message", acknowledgeHeader);
+ for (Map.Entry entry : acknowledgeMessages.entrySet()) {
+ builder.startArray(entry.getKey());
+ for (String message : entry.getValue()) {
+ builder.value(message);
+ }
+ builder.endArray();
+ }
+ builder.endObject();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return Strings.toString(this, true, true);
+ }
+
+ public static PutLicenseResponse fromXContent(XContentParser parser) throws IOException {
+ return PARSER.parse(parser, null);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ if (!super.equals(o)) return false;
+ PutLicenseResponse that = (PutLicenseResponse) o;
+
+ return status == that.status &&
+ ProtocolUtils.equals(acknowledgeMessages, that.acknowledgeMessages) &&
+ Objects.equals(acknowledgeHeader, that.acknowledgeHeader);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(super.hashCode(), status, ProtocolUtils.hashCode(acknowledgeMessages), acknowledgeHeader);
+ }
+
+
+}
diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/common/ProtocolUtilsTests.java b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/common/ProtocolUtilsTests.java
new file mode 100644
index 0000000000000..214708327388f
--- /dev/null
+++ b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/common/ProtocolUtilsTests.java
@@ -0,0 +1,72 @@
+/*
+ * 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.common;
+
+import org.elasticsearch.test.ESTestCase;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class ProtocolUtilsTests extends ESTestCase {
+
+ public void testMapStringEqualsAndHash() {
+ assertTrue(ProtocolUtils.equals(null, null));
+ assertFalse(ProtocolUtils.equals(null, new HashMap<>()));
+ assertFalse(ProtocolUtils.equals(new HashMap<>(), null));
+
+ Map a = new HashMap<>();
+ a.put("foo", new String[] { "a", "b" });
+ a.put("bar", new String[] { "b", "c" });
+
+ Map b = new HashMap<>();
+ b.put("foo", new String[] { "a", "b" });
+
+ assertFalse(ProtocolUtils.equals(a, b));
+ assertFalse(ProtocolUtils.equals(b, a));
+
+ b.put("bar", new String[] { "c", "b" });
+
+ assertFalse(ProtocolUtils.equals(a, b));
+ assertFalse(ProtocolUtils.equals(b, a));
+
+ b.put("bar", new String[] { "b", "c" });
+
+ assertTrue(ProtocolUtils.equals(a, b));
+ assertTrue(ProtocolUtils.equals(b, a));
+ assertEquals(ProtocolUtils.hashCode(a), ProtocolUtils.hashCode(b));
+
+ b.put("baz", new String[] { "b", "c" });
+
+ assertFalse(ProtocolUtils.equals(a, b));
+ assertFalse(ProtocolUtils.equals(b, a));
+
+ a.put("non", null);
+
+ assertFalse(ProtocolUtils.equals(a, b));
+ assertFalse(ProtocolUtils.equals(b, a));
+
+ b.put("non", null);
+ b.remove("baz");
+
+ assertTrue(ProtocolUtils.equals(a, b));
+ assertTrue(ProtocolUtils.equals(b, a));
+ assertEquals(ProtocolUtils.hashCode(a), ProtocolUtils.hashCode(b));
+ }
+}
diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/license/PutLicenseResponseTests.java b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/license/PutLicenseResponseTests.java
new file mode 100644
index 0000000000000..3b7a3e086b2a9
--- /dev/null
+++ b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/license/PutLicenseResponseTests.java
@@ -0,0 +1,119 @@
+/*
+ * 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.license;
+
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.test.AbstractStreamableXContentTestCase;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+public class PutLicenseResponseTests extends AbstractStreamableXContentTestCase {
+
+ @Override
+ protected boolean supportsUnknownFields() {
+ // The structure of the response is such that unknown fields cannot be supported since they are treated as messages from
+ // new services
+ return false;
+ }
+
+ @Override
+ protected PutLicenseResponse createTestInstance() {
+ boolean acknowledged = randomBoolean();
+ LicensesStatus status = randomFrom(LicensesStatus.VALID, LicensesStatus.INVALID, LicensesStatus.EXPIRED);
+ String messageHeader;
+ Map ackMessages;
+ if (randomBoolean()) {
+ messageHeader = randomAlphaOfLength(10);
+ ackMessages = randomAckMessages();
+ } else {
+ messageHeader = null;
+ ackMessages = Collections.emptyMap();
+ }
+
+ return new PutLicenseResponse(acknowledged, status, messageHeader, ackMessages);
+ }
+
+ private static Map randomAckMessages() {
+ int nFeatures = randomIntBetween(1, 5);
+
+ Map ackMessages = new HashMap<>();
+
+ for (int i = 0; i < nFeatures; i++) {
+ String feature = randomAlphaOfLengthBetween(9, 15);
+ int nMessages = randomIntBetween(1, 5);
+ String[] messages = new String[nMessages];
+ for (int j = 0; j < nMessages; j++) {
+ messages[j] = randomAlphaOfLengthBetween(10, 30);
+ }
+ ackMessages.put(feature, messages);
+ }
+
+ return ackMessages;
+ }
+
+ @Override
+ protected PutLicenseResponse doParseInstance(XContentParser parser) throws IOException {
+ return PutLicenseResponse.fromXContent(parser);
+ }
+
+ @Override
+ protected PutLicenseResponse createBlankInstance() {
+ return new PutLicenseResponse();
+ }
+
+ @Override
+ protected PutLicenseResponse mutateInstance(PutLicenseResponse response) {
+ @SuppressWarnings("unchecked")
+ Function mutator = randomFrom(
+ r -> new PutLicenseResponse(
+ r.isAcknowledged() == false,
+ r.status(),
+ r.acknowledgeHeader(),
+ r.acknowledgeMessages()),
+ r -> new PutLicenseResponse(
+ r.isAcknowledged(),
+ mutateStatus(r.status()),
+ r.acknowledgeHeader(),
+ r.acknowledgeMessages()),
+ r -> {
+ if (r.acknowledgeMessages().isEmpty()) {
+ return new PutLicenseResponse(
+ r.isAcknowledged(),
+ r.status(),
+ randomAlphaOfLength(10),
+ randomAckMessages()
+ );
+ } else {
+ return new PutLicenseResponse(r.isAcknowledged(), r.status());
+ }
+ }
+
+ );
+ return mutator.apply(response);
+ }
+
+ private LicensesStatus mutateStatus(LicensesStatus status) {
+ return randomValueOtherThan(status, () -> randomFrom(LicensesStatus.values()));
+ }
+
+}