Browse Source

add start trial API to HLRC (#33406)

Introduces client-specific request and response classes that do not
depend on the server

The `type` parameter is named `licenseType` in the response class to be
more descriptive. The parts that make up the acknowledged-required
response are given slightly different names than their server-response
types to be consistent with the naming in the put license API

Tests do not cover all cases because the integ test cluster starts up
with a trial license - this will be addressed in a future commit
Andy Bristol 7 years ago
parent
commit
18aa1c1381

+ 27 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/LicenseClient.java

@@ -22,6 +22,8 @@ package org.elasticsearch.client;
 import org.apache.http.HttpEntity;
 import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.support.master.AcknowledgedResponse;
+import org.elasticsearch.client.license.StartTrialRequest;
+import org.elasticsearch.client.license.StartTrialResponse;
 import org.elasticsearch.client.license.StartBasicRequest;
 import org.elasticsearch.client.license.StartBasicResponse;
 import org.elasticsearch.common.Strings;
@@ -44,6 +46,7 @@ import java.io.InputStreamReader;
 import java.nio.charset.StandardCharsets;
 
 import static java.util.Collections.emptySet;
+import static java.util.Collections.singleton;
 
 /**
  * A wrapper for the {@link RestHighLevelClient} that provides methods for
@@ -123,6 +126,30 @@ public final class LicenseClient {
             AcknowledgedResponse::fromXContent, listener, emptySet());
     }
 
+    /**
+     * Starts a trial license on 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 StartTrialResponse startTrial(StartTrialRequest request, RequestOptions options) throws IOException {
+        return restHighLevelClient.performRequestAndParseEntity(request, LicenseRequestConverters::startTrial, options,
+            StartTrialResponse::fromXContent, singleton(403));
+    }
+
+    /**
+     * Asynchronously starts a trial license on the 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 startTrialAsync(StartTrialRequest request,
+                                RequestOptions options,
+                                ActionListener<StartTrialResponse> listener) {
+
+        restHighLevelClient.performRequestAsyncAndParseEntity(request, LicenseRequestConverters::startTrial, options,
+            StartTrialResponse::fromXContent, listener, singleton(403));
+    }
+
     /**
      * Initiates an indefinite basic license.
      * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized

+ 17 - 9
client/rest-high-level/src/main/java/org/elasticsearch/client/LicenseRequestConverters.java

@@ -23,6 +23,7 @@ import org.apache.http.client.methods.HttpDelete;
 import org.apache.http.client.methods.HttpGet;
 import org.apache.http.client.methods.HttpPost;
 import org.apache.http.client.methods.HttpPut;
+import org.elasticsearch.client.license.StartTrialRequest;
 import org.elasticsearch.client.license.StartBasicRequest;
 import org.elasticsearch.protocol.xpack.license.DeleteLicenseRequest;
 import org.elasticsearch.protocol.xpack.license.GetLicenseRequest;
@@ -30,10 +31,7 @@ import org.elasticsearch.protocol.xpack.license.PutLicenseRequest;
 
 public class LicenseRequestConverters {
     static Request putLicense(PutLicenseRequest putLicenseRequest) {
-        String endpoint = new RequestConverters.EndpointBuilder()
-            .addPathPartAsIs("_xpack")
-            .addPathPartAsIs("license")
-            .build();
+        String endpoint = new RequestConverters.EndpointBuilder().addPathPartAsIs("_xpack", "license").build();
         Request request = new Request(HttpPut.METHOD_NAME, endpoint);
         RequestConverters.Params parameters = new RequestConverters.Params(request);
         parameters.withTimeout(putLicenseRequest.timeout());
@@ -46,10 +44,7 @@ public class LicenseRequestConverters {
     }
 
     static Request getLicense(GetLicenseRequest getLicenseRequest) {
-        String endpoint = new RequestConverters.EndpointBuilder()
-            .addPathPartAsIs("_xpack")
-            .addPathPartAsIs("license")
-            .build();
+        String endpoint = new RequestConverters.EndpointBuilder().addPathPartAsIs("_xpack", "license").build();
         Request request = new Request(HttpGet.METHOD_NAME, endpoint);
         RequestConverters.Params parameters = new RequestConverters.Params(request);
         parameters.withLocal(getLicenseRequest.local());
@@ -57,13 +52,26 @@ public class LicenseRequestConverters {
     }
 
     static Request deleteLicense(DeleteLicenseRequest deleteLicenseRequest) {
-        Request request = new Request(HttpDelete.METHOD_NAME, "/_xpack/license");
+        String endpoint = new RequestConverters.EndpointBuilder().addPathPartAsIs("_xpack", "license").build();
+        Request request = new Request(HttpDelete.METHOD_NAME, endpoint);
         RequestConverters.Params parameters = new RequestConverters.Params(request);
         parameters.withTimeout(deleteLicenseRequest.timeout());
         parameters.withMasterTimeout(deleteLicenseRequest.masterNodeTimeout());
         return request;
     }
 
+    static Request startTrial(StartTrialRequest startTrialRequest) {
+        final String endpoint = new RequestConverters.EndpointBuilder().addPathPartAsIs("_xpack", "license", "start_trial").build();
+        final Request request = new Request(HttpPost.METHOD_NAME, endpoint);
+
+        RequestConverters.Params parameters = new RequestConverters.Params(request);
+        parameters.putParam("acknowledge", Boolean.toString(startTrialRequest.isAcknowledge()));
+        if (startTrialRequest.getLicenseType() != null) {
+            parameters.putParam("type", startTrialRequest.getLicenseType());
+        }
+        return request;
+    }
+
     static Request startBasic(StartBasicRequest startBasicRequest) {
         String endpoint = new RequestConverters.EndpointBuilder()
             .addPathPartAsIs("_xpack", "license", "start_basic")

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

@@ -980,7 +980,7 @@ final class RequestConverters {
             return this;
         }
 
-        EndpointBuilder addPathPartAsIs(String ... parts) {
+        EndpointBuilder addPathPartAsIs(String... parts) {
             for (String part : parts) {
                 if (Strings.hasLength(part)) {
                     joiner.add(part);

+ 50 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/license/StartTrialRequest.java

@@ -0,0 +1,50 @@
+/*
+ * 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.license;
+
+import org.elasticsearch.client.Validatable;
+import org.elasticsearch.common.Nullable;
+
+public class StartTrialRequest implements Validatable {
+
+    private final boolean acknowledge;
+    private final String licenseType;
+
+    public StartTrialRequest() {
+        this(false);
+    }
+
+    public StartTrialRequest(boolean acknowledge) {
+        this(acknowledge, null);
+    }
+
+    public StartTrialRequest(boolean acknowledge, @Nullable String licenseType) {
+        this.acknowledge = acknowledge;
+        this.licenseType = licenseType;
+    }
+
+    public boolean isAcknowledge() {
+        return acknowledge;
+    }
+
+    public String getLicenseType() {
+        return licenseType;
+    }
+}

+ 188 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/license/StartTrialResponse.java

@@ -0,0 +1,188 @@
+/*
+ * 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.license;
+
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.collect.Tuple;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.XContentParseException;
+import org.elasticsearch.common.xcontent.XContentParser;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
+
+public class StartTrialResponse {
+
+    private static final ConstructingObjectParser<StartTrialResponse, Void> PARSER = new ConstructingObjectParser<>(
+        "start_trial_response",
+        true,
+        (Object[] arguments, Void aVoid) -> {
+            final boolean acknowledged = (boolean) arguments[0];
+            final boolean trialWasStarted = (boolean) arguments[1];
+            final String licenseType = (String) arguments[2];
+            final String errorMessage = (String) arguments[3];
+
+            @SuppressWarnings("unchecked")
+            final Tuple<String, Map<String, String[]>> acknowledgeDetails = (Tuple<String, Map<String, String[]>>) arguments[4];
+            final String acknowledgeHeader;
+            final Map<String, String[]> acknowledgeMessages;
+
+            if (acknowledgeDetails != null) {
+                acknowledgeHeader = acknowledgeDetails.v1();
+                acknowledgeMessages = acknowledgeDetails.v2();
+            } else {
+                acknowledgeHeader = null;
+                acknowledgeMessages = null;
+            }
+
+            return new StartTrialResponse(acknowledged, trialWasStarted, licenseType, errorMessage, acknowledgeHeader,
+                acknowledgeMessages);
+        }
+    );
+
+    static {
+        PARSER.declareBoolean(constructorArg(), new ParseField("acknowledged"));
+        PARSER.declareBoolean(constructorArg(), new ParseField("trial_was_started"));
+        PARSER.declareString(optionalConstructorArg(), new ParseField("type"));
+        PARSER.declareString(optionalConstructorArg(), new ParseField("error_message"));
+        // todo consolidate this parsing with the parsing in PutLicenseResponse
+        PARSER.declareObject(optionalConstructorArg(), (parser, aVoid) -> {
+            final Map<String, String[]> acknowledgeMessages = new HashMap<>();
+            String message = null;
+
+            final Map<String, Object> parsedMap = parser.map();
+            for (Map.Entry<String, Object> entry : parsedMap.entrySet()) {
+                if (entry.getKey().equals("message")) {
+                    if (entry.getValue() instanceof String) {
+                        message = (String) entry.getValue();
+                    } else {
+                        throw new XContentParseException(parser.getTokenLocation(), "unexpected acknowledgement header type");
+                    }
+                } else {
+                    if (entry.getValue() instanceof List) {
+                        final List<String> messageStrings = new ArrayList<>();
+                        @SuppressWarnings("unchecked")
+                        final List<Object> messageObjects = (List<Object>) entry.getValue();
+                        for (Object messageObject : messageObjects) {
+                            if (messageObject instanceof String) {
+                                messageStrings.add((String) messageObject);
+                            } else {
+                                throw new XContentParseException(parser.getTokenLocation(), "expected text in acknowledgement message");
+                            }
+                        }
+
+                        acknowledgeMessages.put(entry.getKey(), messageStrings.toArray(new String[messageStrings.size()]));
+                    } else {
+                        throw new XContentParseException(parser.getTokenLocation(), "unexpected acknowledgement message type");
+                    }
+                }
+            }
+
+            if (message == null) {
+                throw new XContentParseException(parser.getTokenLocation(), "expected acknowledgement header");
+            }
+
+            return new Tuple<>(message, acknowledgeMessages);
+
+        }, new ParseField("acknowledge"));
+    }
+
+    public static StartTrialResponse fromXContent(XContentParser parser) throws IOException {
+        return PARSER.apply(parser, null);
+    }
+
+    private final boolean acknowledged;
+    private final boolean trialWasStarted;
+    private final String licenseType;
+    private final String errorMessage;
+    private final String acknowledgeHeader;
+    private final Map<String, String[]> acknowledgeMessages;
+
+    public StartTrialResponse(boolean acknowledged,
+                              boolean trialWasStarted,
+                              String licenseType,
+                              String errorMessage,
+                              String acknowledgeHeader,
+                              Map<String, String[]> acknowledgeMessages) {
+
+        this.acknowledged = acknowledged;
+        this.trialWasStarted = trialWasStarted;
+        this.licenseType = licenseType;
+        this.errorMessage = errorMessage;
+        this.acknowledgeHeader = acknowledgeHeader;
+        this.acknowledgeMessages = acknowledgeMessages;
+    }
+
+    /**
+     * Returns true if the request that corresponds to this response acknowledged license changes that would occur as a result of starting
+     * a trial license
+     */
+    public boolean isAcknowledged() {
+        return acknowledged;
+    }
+
+    /**
+     * Returns true if a trial license was started as a result of the request corresponding to this response. Returns false if the cluster
+     * did not start a trial, or a trial had already been started before the corresponding request was made
+     */
+    public boolean isTrialWasStarted() {
+        return trialWasStarted;
+    }
+
+    /**
+     * If a trial license was started as a result of the request corresponding to this response (see {@link #isTrialWasStarted()}) then
+     * returns the type of license that was started on the cluster. Returns null otherwise
+     */
+    public String getLicenseType() {
+        return licenseType;
+    }
+
+    /**
+     * If a trial license was not started as a result of the request corresponding to this response (see {@link #isTrialWasStarted()} then
+     * returns a brief message explaining why the trial could not be started. Returns false otherwise
+     */
+    public String getErrorMessage() {
+        return errorMessage;
+    }
+
+    /**
+     * If the request corresponding to this response did not acknowledge licensing changes that would result from starting a trial license
+     * (see {@link #isAcknowledged()}), returns a message describing how the user must acknowledge licensing changes as a result of
+     * such a request. Returns null otherwise
+     */
+    public String getAcknowledgeHeader() {
+        return acknowledgeHeader;
+    }
+
+    /**
+     * If the request corresponding to this response did not acknowledge licensing changes that would result from starting a trial license
+     * (see {@link #isAcknowledged()}, returns a map. The map's keys are names of commercial Elasticsearch features, and their values are
+     * messages about how those features will be affected by licensing changes as a result of starting a trial license
+     */
+    public Map<String, String[]> getAcknowledgeMessages() {
+        return acknowledgeMessages;
+    }
+}

+ 49 - 11
client/rest-high-level/src/test/java/org/elasticsearch/client/LicensingIT.java → client/rest-high-level/src/test/java/org/elasticsearch/client/LicenseIT.java

@@ -16,11 +16,14 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+
 package org.elasticsearch.client;
 
 import org.elasticsearch.Build;
 import org.elasticsearch.client.license.StartBasicRequest;
 import org.elasticsearch.client.license.StartBasicResponse;
+import org.elasticsearch.client.license.StartTrialRequest;
+import org.elasticsearch.client.license.StartTrialResponse;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.collect.MapBuilder;
 import org.elasticsearch.protocol.xpack.license.LicensesStatus;
@@ -31,7 +34,6 @@ import org.junit.BeforeClass;
 
 import java.io.IOException;
 import java.util.Arrays;
-import java.util.List;
 import java.util.Map;
 
 import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
@@ -42,7 +44,13 @@ import static org.hamcrest.Matchers.isEmptyOrNullString;
 import static org.hamcrest.Matchers.not;
 import static org.hamcrest.Matchers.empty;
 
-public class LicensingIT extends ESRestHighLevelClientTestCase {
+public class LicenseIT extends ESRestHighLevelClientTestCase {
+
+    /*
+     * todo there are some cases we can't test here because this gradle project starts the integ test cluster so that it'll generate
+     * a trial license at startup. we need to add a separate gradle project for the license-related tests so that we can start the
+     * integ test cluster without generating a trial license
+     */
 
     @BeforeClass
     public static void checkForSnapshot() {
@@ -55,6 +63,37 @@ public class LicensingIT extends ESRestHighLevelClientTestCase {
         putTrialLicense();
     }
 
+    public void testStartTrial() throws Exception {
+
+        // todo add case where we successfully start a trial - see note above
+
+        // case where we don't acknowledge trial license conditions
+        {
+            final StartTrialRequest request = new StartTrialRequest();
+            final StartTrialResponse response = highLevelClient().license().startTrial(request, RequestOptions.DEFAULT);
+
+            assertThat(response.isAcknowledged(), equalTo(false));
+            assertThat(response.isTrialWasStarted(), equalTo(false));
+            assertThat(response.getLicenseType(), nullValue());
+            assertThat(response.getErrorMessage(), equalTo("Operation failed: Needs acknowledgement."));
+            assertThat(response.getAcknowledgeHeader(), containsString("This API initiates a free 30-day trial for all platinum features"));
+            assertNotEmptyAcknowledgeMessages(response.getAcknowledgeMessages());
+        }
+
+        // case where we acknowledge, but the trial is already started at cluster startup
+        {
+            final StartTrialRequest request = new StartTrialRequest(true);
+            final StartTrialResponse response = highLevelClient().license().startTrial(request, RequestOptions.DEFAULT);
+
+            assertThat(response.isAcknowledged(), equalTo(true));
+            assertThat(response.isTrialWasStarted(), equalTo(false));
+            assertThat(response.getLicenseType(), nullValue());
+            assertThat(response.getErrorMessage(), equalTo("Operation failed: Trial was already activated."));
+            assertThat(response.getAcknowledgeHeader(), nullValue());
+            assertThat(response.getAcknowledgeMessages(), nullValue());
+        }
+    }
+
     public static void putTrialLicense() throws IOException {
         assumeTrue("Trial license is only valid when tested against snapshot/test builds",
             Build.CURRENT.isSnapshot());
@@ -76,9 +115,9 @@ public class LicensingIT extends ESRestHighLevelClientTestCase {
                     .put("start_date_in_millis", "-1")
                     .put("signature",
                         "AAAABAAAAA3FXON9kGmNqmH+ASDWAAAAIAo5/x6hrsGh1GqqrJmy4qgmEC7gK0U4zQ6q5ZEMhm4jAAABAAcdKHL0BfM2uqTgT7BDuFxX5lb"
-                        + "t/bHDVJ421Wwgm5p3IMbw/W13iiAHz0hhDziF7acJbc/y65L+BKGtVC1gSSHeLDHaAD66VrjKxfc7VbGyJIAYBOdujf0rheurmaD3IcNo"
-                        + "/tWDjCdtTwrNziFkorsGcPadBP5Yc6csk3/Q74DlfiYweMBxLUfkBERwxwd5OQS6ujGvl/4bb8p5zXvOw8vMSaAXSXXnExP6lam+0934W"
-                        + "0kHvU7IGk+fCUjOaiSWKSoE4TEcAtVNYj/oRoRtfQ1KQGpdCHxTHs1BimdZaG0nBHDsvhYlVVLSvHN6QzqsHWgFDG6JJxhtU872oTRSUHA=")
+                            + "t/bHDVJ421Wwgm5p3IMbw/W13iiAHz0hhDziF7acJbc/y65L+BKGtVC1gSSHeLDHaAD66VrjKxfc7VbGyJIAYBOdujf0rheurmaD3IcNo"
+                            + "/tWDjCdtTwrNziFkorsGcPadBP5Yc6csk3/Q74DlfiYweMBxLUfkBERwxwd5OQS6ujGvl/4bb8p5zXvOw8vMSaAXSXXnExP6lam+0934W"
+                            + "0kHvU7IGk+fCUjOaiSWKSoE4TEcAtVNYj/oRoRtfQ1KQGpdCHxTHs1BimdZaG0nBHDsvhYlVVLSvHN6QzqsHWgFDG6JJxhtU872oTRSUHA=")
                     .immutableMap()))
             .endObject());
 
@@ -103,7 +142,7 @@ public class LicensingIT extends ESRestHighLevelClientTestCase {
             assertThat(response.getAcknowledgeMessage(),
                 containsString("This license update requires acknowledgement. " +
                     "To acknowledge the license, please read the following messages and call /start_basic again"));
-            assertNotEmptyAcknowledgeMessages(response);
+            assertNotEmptyAcknowledgeMessages(response.getAcknowledgeMessages());
         }
         // case where we acknowledge and the basic is started successfully
         {
@@ -117,12 +156,11 @@ public class LicensingIT extends ESRestHighLevelClientTestCase {
         }
     }
 
-    private static void assertNotEmptyAcknowledgeMessages(StartBasicResponse response) {
-        assertThat(response.getAcknowledgeMessages().entrySet(), not(empty()));
-        for (Map.Entry<String, String[]> entry : response.getAcknowledgeMessages().entrySet()) {
+    private static void assertNotEmptyAcknowledgeMessages(Map<String, String[]> acknowledgeMessages) {
+        assertThat(acknowledgeMessages.entrySet(), not(empty()));
+        for (Map.Entry<String, String[]> entry : acknowledgeMessages.entrySet()) {
             assertThat(entry.getKey(), not(isEmptyOrNullString()));
-            final List<String> messages = Arrays.asList(entry.getValue());
-            for (String message : messages) {
+            for (String message : entry.getValue()) {
                 assertThat(message, not(isEmptyOrNullString()));
             }
         }

+ 27 - 2
client/rest-high-level/src/test/java/org/elasticsearch/client/LicenseRequestConvertersTests.java

@@ -16,9 +16,11 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+
 package org.elasticsearch.client;
 
 import org.apache.http.client.methods.HttpPost;
+import org.elasticsearch.client.license.StartTrialRequest;
 import org.elasticsearch.action.support.master.AcknowledgedRequest;
 import org.elasticsearch.client.license.StartBasicRequest;
 import org.elasticsearch.test.ESTestCase;
@@ -26,13 +28,36 @@ import org.elasticsearch.test.ESTestCase;
 import java.util.HashMap;
 import java.util.Map;
 
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.nullValue;
 import static org.elasticsearch.client.RequestConvertersTests.setRandomMasterTimeout;
 import static org.elasticsearch.client.RequestConvertersTests.setRandomTimeout;
-import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.nullValue;
+
 
 public class LicenseRequestConvertersTests extends ESTestCase {
+
+    public void testStartTrial() {
+        final boolean acknowledge = randomBoolean();
+        final String licenseType = randomBoolean()
+            ? randomAlphaOfLengthBetween(3, 10)
+            : null;
+
+        final Map<String, String> expectedParams = new HashMap<>();
+        expectedParams.put("acknowledge", Boolean.toString(acknowledge));
+        if (licenseType != null) {
+            expectedParams.put("type", licenseType);
+        }
+
+        final StartTrialRequest hlrcRequest = new StartTrialRequest(acknowledge, licenseType);
+        final Request restRequest = LicenseRequestConverters.startTrial(hlrcRequest);
+
+        assertThat(restRequest.getMethod(), equalTo(HttpPost.METHOD_NAME));
+        assertThat(restRequest.getEndpoint(), equalTo("/_xpack/license/start_trial"));
+        assertThat(restRequest.getParameters(), equalTo(expectedParams));
+        assertThat(restRequest.getEntity(), nullValue());
+    }
+
     public void testStartBasic() {
         final boolean acknowledge = randomBoolean();
         StartBasicRequest startBasicRequest = new StartBasicRequest(acknowledge);

+ 60 - 1
client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/LicensingDocumentationIT.java

@@ -26,6 +26,8 @@ import org.elasticsearch.action.support.master.AcknowledgedResponse;
 import org.elasticsearch.client.ESRestHighLevelClientTestCase;
 import org.elasticsearch.client.RequestOptions;
 import org.elasticsearch.client.RestHighLevelClient;
+import org.elasticsearch.client.license.StartTrialRequest;
+import org.elasticsearch.client.license.StartTrialResponse;
 import org.elasticsearch.client.license.StartBasicRequest;
 import org.elasticsearch.client.license.StartBasicResponse;
 import org.elasticsearch.common.Booleans;
@@ -43,12 +45,14 @@ import java.util.Map;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
-import static org.elasticsearch.client.LicensingIT.putTrialLicense;
+import static org.elasticsearch.client.LicenseIT.putTrialLicense;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.endsWith;
 import static org.hamcrest.Matchers.hasSize;
 import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.nullValue;
 import static org.hamcrest.Matchers.startsWith;
+import static org.hamcrest.core.Is.is;
 
 /**
  * Documentation for Licensing APIs in the high level java client.
@@ -232,6 +236,61 @@ public class LicensingDocumentationIT extends ESRestHighLevelClientTestCase {
         }
     }
 
+    public void testStartTrial() throws Exception {
+        RestHighLevelClient client = highLevelClient();
+
+        {
+            // tag::start-trial-execute
+            StartTrialRequest request = new StartTrialRequest(true); // <1>
+
+            StartTrialResponse response = client.license().startTrial(request, RequestOptions.DEFAULT);
+            // end::start-trial-execute
+
+            // tag::start-trial-response
+            boolean acknowledged = response.isAcknowledged();                              // <1>
+            boolean trialWasStarted = response.isTrialWasStarted();                        // <2>
+            String licenseType = response.getLicenseType();                                // <3>
+            String errorMessage = response.getErrorMessage();                              // <4>
+            String acknowledgeHeader = response.getAcknowledgeHeader();                    // <5>
+            Map<String, String[]> acknowledgeMessages = response.getAcknowledgeMessages(); // <6>
+            // end::start-trial-response
+
+            assertTrue(acknowledged);
+            assertFalse(trialWasStarted);
+            assertThat(licenseType, nullValue());
+            assertThat(errorMessage, is("Operation failed: Trial was already activated."));
+            assertThat(acknowledgeHeader, nullValue());
+            assertThat(acknowledgeMessages, nullValue());
+        }
+
+        {
+            StartTrialRequest request = new StartTrialRequest();
+
+            // tag::start-trial-execute-listener
+            ActionListener<StartTrialResponse> listener = new ActionListener<StartTrialResponse>() {
+                @Override
+                public void onResponse(StartTrialResponse response) {
+                    // <1>
+                }
+
+                @Override
+                public void onFailure(Exception e) {
+                    // <2>
+                }
+            };
+            // end::start-trial-execute-listener
+
+            final CountDownLatch latch = new CountDownLatch(1);
+            listener = new LatchedActionListener<>(listener, latch);
+
+            // tag::start-trial-execute-async
+            client.license().startTrialAsync(request, RequestOptions.DEFAULT, listener);
+            // end::start-trial-execute-async
+
+            assertTrue(latch.await(30L, TimeUnit.SECONDS));
+        }
+    }
+
     public void testPostStartBasic() throws Exception {
         RestHighLevelClient client = highLevelClient();
         {

+ 70 - 0
docs/java-rest/high-level/licensing/start-trial.asciidoc

@@ -0,0 +1,70 @@
+[[java-rest-high-start-trial]]
+=== Start Trial
+
+[[java-rest-high-start-trial-execution]]
+==== Execution
+
+This API creates and enables a trial license using the `startTrial()`
+method.
+
+["source","java",subs="attributes,callouts,macros"]
+---------------------------------------------------
+include-tagged::{doc-tests}/LicensingDocumentationIT.java[start-trial-execute]
+---------------------------------------------------
+<1> Sets the "acknowledge" parameter to true, indicating the user has
+acknowledged that starting a trial license may affect commercial features
+
+[[java-rest-high-start-trial-response]]
+==== Response
+
+The returned `StartTrialResponse` returns a field indicating whether the
+trial was started. If it was started, the response returns a the type of
+license started. If it was not started, it returns an error message describing
+why.
+
+Acknowledgement messages may also be returned if this API was called without
+the `acknowledge` flag set to `true`.  In this case you need to display the
+messages to the end user and if they agree, resubmit the request with the
+`acknowledge` flag set to `true`. Please note that the response will still
+return a 200 return code even if it requires an acknowledgement. So, it is
+necessary to check the `acknowledged` flag.
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/LicensingDocumentationIT.java[start-trial-response]
+--------------------------------------------------
+<1> Whether or not the request had the `acknowledge` flag set
+<2> Whether or not this request caused a trial to start
+<3> If this request caused a trial to start, which type of license it
+registered
+<4> If this request did not cause a trial to start, a message explaining why
+<5> If the user's request did not have the `acknowledge` flag set, a summary
+of the user's acknowledgement required for this API
+<6> If the user's request did not have the `acknowledge` flag set, contains
+keys of commercial features and values of messages describing how they will
+be affected by licensing changes as the result of starting a trial
+
+[[java-rest-high-start-trial-async]]
+==== Asynchronous execution
+
+This request can be executed asynchronously:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/LicensingDocumentationIT.java[start-trial-execute-async]
+--------------------------------------------------
+
+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 `StartTrialResponse` looks like:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests}/LicensingDocumentationIT.java[start-trial-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 - 0
docs/java-rest/high-level/supported-apis.asciidoc

@@ -214,11 +214,13 @@ The Java High Level REST Client supports the following Licensing APIs:
 * <<java-rest-high-put-license>>
 * <<java-rest-high-get-license>>
 * <<java-rest-high-delete-license>>
+* <<java-rest-high-start-trial>>
 * <<java-rest-high-start-basic>>
 
 include::licensing/put-license.asciidoc[]
 include::licensing/get-license.asciidoc[]
 include::licensing/delete-license.asciidoc[]
+include::licensing/start-trial.asciidoc[]
 include::licensing/start-basic.asciidoc[]
 
 == Machine Learning APIs