From 90a8e4b259f4d7590cf90594742cfc65e2c45e57 Mon Sep 17 00:00:00 2001
From: Benjamin Trent <ben.w.trent@gmail.com>
Date: Wed, 21 Nov 2018 16:22:04 -0600
Subject: [PATCH] HLRC: ML Delete event from Calendar (#35760)

* HLRC: Delete event from calendar

* adjusting tests

* adjusting code to make it more readable
---
 .../client/MLRequestConverters.java           | 13 ++++
 .../client/MachineLearningClient.java         | 43 ++++++++++
 .../client/ml/DeleteCalendarEventRequest.java | 78 +++++++++++++++++++
 .../client/MLRequestConvertersTests.java      | 10 +++
 .../client/MachineLearningIT.java             | 39 ++++++++++
 .../MlClientDocumentationIT.java              | 67 +++++++++++++++-
 .../ml/DeleteCalendarEventRequestTests.java   | 37 +++++++++
 .../ml/delete-calendar-event.asciidoc         | 36 +++++++++
 .../ml/delete-calendar-job.asciidoc           |  2 +-
 .../high-level/supported-apis.asciidoc        |  2 +
 10 files changed, 325 insertions(+), 2 deletions(-)
 create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteCalendarEventRequest.java
 create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteCalendarEventRequestTests.java
 create mode 100644 docs/java-rest/high-level/ml/delete-calendar-event.asciidoc

diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java
index 15c2b1617d2..57c40852078 100644
--- a/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java
+++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java
@@ -28,6 +28,7 @@ import org.apache.http.entity.ByteArrayEntity;
 import org.apache.lucene.util.BytesRef;
 import org.elasticsearch.client.RequestConverters.EndpointBuilder;
 import org.elasticsearch.client.ml.CloseJobRequest;
+import org.elasticsearch.client.ml.DeleteCalendarEventRequest;
 import org.elasticsearch.client.ml.DeleteCalendarJobRequest;
 import org.elasticsearch.client.ml.DeleteCalendarRequest;
 import org.elasticsearch.client.ml.DeleteDatafeedRequest;
@@ -584,6 +585,18 @@ final class MLRequestConverters {
         return request;
     }
 
+    static Request deleteCalendarEvent(DeleteCalendarEventRequest deleteCalendarEventRequest) {
+        String endpoint = new EndpointBuilder()
+            .addPathPartAsIs("_xpack")
+            .addPathPartAsIs("ml")
+            .addPathPartAsIs("calendars")
+            .addPathPart(deleteCalendarEventRequest.getCalendarId())
+            .addPathPartAsIs("events")
+            .addPathPart(deleteCalendarEventRequest.getEventId())
+            .build();
+        return new Request(HttpDelete.METHOD_NAME, endpoint);
+    }
+
     static Request putFilter(PutFilterRequest putFilterRequest) throws IOException {
         String endpoint = new EndpointBuilder()
             .addPathPartAsIs("_xpack")
diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java
index bba67a792da..204dfaa87bc 100644
--- a/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java
+++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java
@@ -22,6 +22,7 @@ import org.elasticsearch.action.ActionListener;
 import org.elasticsearch.action.support.master.AcknowledgedResponse;
 import org.elasticsearch.client.ml.CloseJobRequest;
 import org.elasticsearch.client.ml.CloseJobResponse;
+import org.elasticsearch.client.ml.DeleteCalendarEventRequest;
 import org.elasticsearch.client.ml.DeleteCalendarJobRequest;
 import org.elasticsearch.client.ml.DeleteCalendarRequest;
 import org.elasticsearch.client.ml.DeleteDatafeedRequest;
@@ -1513,6 +1514,48 @@ public final class MachineLearningClient {
             Collections.emptySet());
     }
 
+    /**
+     * Removes a Scheduled Event from a calendar
+     * <p>
+     * For additional info
+     * see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-delete-calendar-event.html">
+     * ML Delete calendar event documentation</a>
+     *
+     * @param request The request
+     * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+     * @return The {@link PutCalendarResponse} containing the updated calendar
+     * @throws IOException when there is a serialization issue sending the request or receiving the response
+     */
+    public AcknowledgedResponse deleteCalendarEvent(DeleteCalendarEventRequest request, RequestOptions options) throws IOException {
+        return restHighLevelClient.performRequestAndParseEntity(request,
+            MLRequestConverters::deleteCalendarEvent,
+            options,
+            AcknowledgedResponse::fromXContent,
+            Collections.emptySet());
+    }
+
+    /**
+     * Removes a Scheduled Event from a calendar, notifies listener when completed
+     * <p>
+     * For additional info
+     * see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-delete-calendar-event.html">
+     * ML Delete calendar event documentation</a>
+     *
+     * @param request  The request
+     * @param options  Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+     * @param listener Listener to be notified upon request completion
+     */
+    public void deleteCalendarEventAsync(DeleteCalendarEventRequest request,
+                                         RequestOptions options,
+                                         ActionListener<AcknowledgedResponse> listener) {
+        restHighLevelClient.performRequestAsyncAndParseEntity(request,
+            MLRequestConverters::deleteCalendarEvent,
+            options,
+            AcknowledgedResponse::fromXContent,
+            listener,
+            Collections.emptySet());
+    }
+
     /**
      * Creates a new Machine Learning Filter
      * <p>
diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteCalendarEventRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteCalendarEventRequest.java
new file mode 100644
index 00000000000..b10d3cc7344
--- /dev/null
+++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteCalendarEventRequest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.ml;
+
+import org.elasticsearch.action.ActionRequest;
+import org.elasticsearch.action.ActionRequestValidationException;
+
+import java.util.Objects;
+
+/**
+ * Request class for removing an event from an existing calendar
+ */
+public class DeleteCalendarEventRequest extends ActionRequest {
+
+    private final String eventId;
+    private final String calendarId;
+
+    /**
+     * Create a new request referencing an existing Calendar and which event to remove
+     * from it.
+     *
+     * @param calendarId The non-null ID of the calendar
+     * @param eventId Scheduled Event to remove from the calendar, Cannot be null.
+     */
+    public DeleteCalendarEventRequest(String calendarId, String eventId) {
+        this.calendarId = Objects.requireNonNull(calendarId, "[calendar_id] must not be null.");
+        this.eventId = Objects.requireNonNull(eventId, "[event_id] must not be null.");
+    }
+
+    public String getEventId() {
+        return eventId;
+    }
+
+    public String getCalendarId() {
+        return calendarId;
+    }
+
+    @Override
+    public ActionRequestValidationException validate() {
+        return null;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(eventId, calendarId);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        }
+
+        if (other == null || getClass() != other.getClass()) {
+            return false;
+        }
+
+        DeleteCalendarEventRequest that = (DeleteCalendarEventRequest) other;
+        return Objects.equals(eventId, that.eventId) &&
+            Objects.equals(calendarId, that.calendarId);
+    }
+}
diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java
index 8bbad5a059a..f5c421c329f 100644
--- a/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java
+++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java
@@ -24,6 +24,7 @@ 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.ml.CloseJobRequest;
+import org.elasticsearch.client.ml.DeleteCalendarEventRequest;
 import org.elasticsearch.client.ml.DeleteCalendarJobRequest;
 import org.elasticsearch.client.ml.DeleteCalendarRequest;
 import org.elasticsearch.client.ml.DeleteDatafeedRequest;
@@ -644,6 +645,15 @@ public class MLRequestConvertersTests extends ESTestCase {
         assertEquals(Strings.toString(builder), requestEntityToString(request));
     }
 
+    public void testDeleteCalendarEvent() {
+        String calendarId = randomAlphaOfLength(10);
+        String eventId = randomAlphaOfLength(5);
+        DeleteCalendarEventRequest deleteCalendarEventRequest = new DeleteCalendarEventRequest(calendarId, eventId);
+        Request request = MLRequestConverters.deleteCalendarEvent(deleteCalendarEventRequest);
+        assertEquals(HttpDelete.METHOD_NAME, request.getMethod());
+        assertEquals("/_xpack/ml/calendars/" + calendarId + "/events/" + eventId, request.getEndpoint());
+    }
+
     public void testPutFilter() throws IOException {
         MlFilter filter = MlFilterTests.createRandomBuilder("foo").build();
         PutFilterRequest putFilterRequest = new PutFilterRequest(filter);
diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java
index 0f4f44b15a0..fe8d5d96b59 100644
--- a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java
+++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java
@@ -29,6 +29,7 @@ import org.elasticsearch.action.support.WriteRequest;
 import org.elasticsearch.action.support.master.AcknowledgedResponse;
 import org.elasticsearch.client.ml.CloseJobRequest;
 import org.elasticsearch.client.ml.CloseJobResponse;
+import org.elasticsearch.client.ml.DeleteCalendarEventRequest;
 import org.elasticsearch.client.ml.DeleteCalendarJobRequest;
 import org.elasticsearch.client.ml.DeleteCalendarRequest;
 import org.elasticsearch.client.ml.DeleteDatafeedRequest;
@@ -119,7 +120,9 @@ import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.hasItem;
 import static org.hamcrest.CoreMatchers.hasItems;
+import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.Matchers.contains;
 import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.hasSize;
@@ -986,6 +989,42 @@ public class MachineLearningIT extends ESRestHighLevelClientTestCase {
         assertThat(postCalendarEventResponse.getScheduledEvents(), containsInAnyOrder(events.toArray()));
     }
 
+    public void testDeleteCalendarEvent() throws IOException {
+        Calendar calendar = CalendarTests.testInstance();
+        MachineLearningClient machineLearningClient = highLevelClient().machineLearning();
+        machineLearningClient.putCalendar(new PutCalendarRequest(calendar), RequestOptions.DEFAULT);
+
+        List<ScheduledEvent> events = new ArrayList<>(3);
+        for (int i = 0; i < 3; i++) {
+            events.add(ScheduledEventTests.testInstance(calendar.getId(), null));
+        }
+
+        machineLearningClient.postCalendarEvent(new PostCalendarEventRequest(calendar.getId(), events), RequestOptions.DEFAULT);
+        GetCalendarEventsResponse getCalendarEventsResponse =
+            machineLearningClient.getCalendarEvents(new GetCalendarEventsRequest(calendar.getId()), RequestOptions.DEFAULT);
+
+        assertThat(getCalendarEventsResponse.events().size(), equalTo(3));
+        String deletedEvent = getCalendarEventsResponse.events().get(0).getEventId();
+
+        DeleteCalendarEventRequest deleteCalendarEventRequest = new DeleteCalendarEventRequest(calendar.getId(), deletedEvent);
+
+        AcknowledgedResponse response = execute(deleteCalendarEventRequest,
+            machineLearningClient::deleteCalendarEvent,
+            machineLearningClient::deleteCalendarEventAsync);
+
+        assertThat(response.isAcknowledged(), is(true));
+
+        getCalendarEventsResponse =
+            machineLearningClient.getCalendarEvents(new GetCalendarEventsRequest(calendar.getId()), RequestOptions.DEFAULT);
+        List<String> remainingIds = getCalendarEventsResponse.events()
+            .stream()
+            .map(ScheduledEvent::getEventId)
+            .collect(Collectors.toList());
+
+        assertThat(remainingIds.size(), equalTo(2));
+        assertThat(remainingIds, not(hasItem(deletedEvent)));
+    }
+
     public void testPutFilter() throws Exception {
         String filterId = "filter-job-test";
         MlFilter mlFilter = MlFilter.builder(filterId)
diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java
index ee3a9eadebf..17f5bcffbcc 100644
--- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java
+++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java
@@ -35,6 +35,7 @@ import org.elasticsearch.client.RequestOptions;
 import org.elasticsearch.client.RestHighLevelClient;
 import org.elasticsearch.client.ml.CloseJobRequest;
 import org.elasticsearch.client.ml.CloseJobResponse;
+import org.elasticsearch.client.ml.DeleteCalendarEventRequest;
 import org.elasticsearch.client.ml.DeleteCalendarJobRequest;
 import org.elasticsearch.client.ml.DeleteCalendarRequest;
 import org.elasticsearch.client.ml.DeleteDatafeedRequest;
@@ -2590,7 +2591,71 @@ public class MlClientDocumentationIT extends ESRestHighLevelClientTestCase {
             assertTrue(latch.await(30L, TimeUnit.SECONDS));
         }
     }
-    
+
+    public void testDeleteCalendarEvent() throws IOException, InterruptedException {
+        RestHighLevelClient client = highLevelClient();
+
+        Calendar calendar = new Calendar("holidays",
+            Arrays.asList("job_1", "job_group_1", "job_2"),
+            "A calendar for public holidays");
+        PutCalendarRequest putRequest = new PutCalendarRequest(calendar);
+        client.machineLearning().putCalendar(putRequest, RequestOptions.DEFAULT);
+        List<ScheduledEvent> events = Arrays.asList(ScheduledEventTests.testInstance(calendar.getId(), null),
+            ScheduledEventTests.testInstance(calendar.getId(), null));
+        client.machineLearning().postCalendarEvent(new PostCalendarEventRequest("holidays", events), RequestOptions.DEFAULT);
+        GetCalendarEventsResponse getCalendarEventsResponse =
+            client.machineLearning().getCalendarEvents(new GetCalendarEventsRequest("holidays"), RequestOptions.DEFAULT);
+        {
+
+            // tag::delete-calendar-event-request
+            DeleteCalendarEventRequest request = new DeleteCalendarEventRequest("holidays", // <1>
+                "EventId"); // <2>
+            // end::delete-calendar-event-request
+
+            request = new DeleteCalendarEventRequest("holidays", getCalendarEventsResponse.events().get(0).getEventId());
+
+            // tag::delete-calendar-event-execute
+            AcknowledgedResponse response = client.machineLearning().deleteCalendarEvent(request, RequestOptions.DEFAULT);
+            // end::delete-calendar-event-execute
+
+            // tag::delete-calendar-event-response
+            boolean acknowledged = response.isAcknowledged(); // <1>
+            // end::delete-calendar-event-response
+
+            assertThat(acknowledged, is(true));
+        }
+        {
+            DeleteCalendarEventRequest request = new DeleteCalendarEventRequest("holidays",
+                getCalendarEventsResponse.events().get(1).getEventId());
+
+            // tag::delete-calendar-event-execute-listener
+            ActionListener<AcknowledgedResponse> listener =
+                new ActionListener<AcknowledgedResponse>() {
+                    @Override
+                    public void onResponse(AcknowledgedResponse deleteCalendarEventResponse) {
+                        // <1>
+                    }
+
+                    @Override
+                    public void onFailure(Exception e) {
+                        // <2>
+                    }
+                };
+            // end::delete-calendar-event-execute-listener
+
+            // Replace the empty listener by a blocking listener in test
+            final CountDownLatch latch = new CountDownLatch(1);
+            listener = new LatchedActionListener<>(listener, latch);
+
+            // tag::delete-calendar-event-execute-async
+            client.machineLearning().deleteCalendarEventAsync(request, RequestOptions.DEFAULT, listener); // <1>
+            // end::delete-calendar-event-execute-async
+
+            assertTrue(latch.await(30L, TimeUnit.SECONDS));
+        }
+    }
+
+
     public void testCreateFilter() throws Exception {
         RestHighLevelClient client = highLevelClient();
         {
diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteCalendarEventRequestTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteCalendarEventRequestTests.java
new file mode 100644
index 00000000000..186ef807245
--- /dev/null
+++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteCalendarEventRequestTests.java
@@ -0,0 +1,37 @@
+/*
+ * 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.ml;
+
+import org.elasticsearch.test.ESTestCase;
+
+public class DeleteCalendarEventRequestTests extends ESTestCase {
+
+    public void testWithNullId() {
+        NullPointerException ex = expectThrows(NullPointerException.class,
+            () -> new DeleteCalendarEventRequest(null, "event1"));
+        assertEquals("[calendar_id] must not be null.", ex.getMessage());
+    }
+
+    public void testWithNullEvent() {
+        NullPointerException ex = expectThrows(NullPointerException.class,
+            () ->new DeleteCalendarEventRequest("calendarId", null));
+        assertEquals("[event_id] must not be null.", ex.getMessage());
+    }
+}
diff --git a/docs/java-rest/high-level/ml/delete-calendar-event.asciidoc b/docs/java-rest/high-level/ml/delete-calendar-event.asciidoc
new file mode 100644
index 00000000000..dcd09a0581d
--- /dev/null
+++ b/docs/java-rest/high-level/ml/delete-calendar-event.asciidoc
@@ -0,0 +1,36 @@
+--
+:api: delete-calendar-event
+:request: DeleteCalendarEventRequest
+:response: AcknowledgedResponse
+--
+[id="{upid}-{api}"]
+=== Delete Calendar Event API
+Removes a scheduled event from an existing {ml} calendar.
+The API accepts a +{request}+ and responds
+with a +{response}+ object.
+
+[id="{upid}-{api}-request"]
+==== Delete Calendar Event Request
+
+A +{request}+ is constructed referencing a non-null
+calendar ID, and eventId which to remove from the calendar
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[{api}-request]
+--------------------------------------------------
+<1> The ID of the calendar from which to remove the jobs
+<2> The eventId to remove from the calendar
+
+[id="{upid}-{api}-response"]
+====  Delete Calendar Event Response
+
+The returned +{response}+ acknowledges the success of the request:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[{api}-response]
+--------------------------------------------------
+<1> Acknowledgement of the request and its success
+
+include::../execution.asciidoc[]
diff --git a/docs/java-rest/high-level/ml/delete-calendar-job.asciidoc b/docs/java-rest/high-level/ml/delete-calendar-job.asciidoc
index d7686315f0f..4e55a221b85 100644
--- a/docs/java-rest/high-level/ml/delete-calendar-job.asciidoc
+++ b/docs/java-rest/high-level/ml/delete-calendar-job.asciidoc
@@ -23,7 +23,7 @@ include-tagged::{doc-tests-file}[{api}-request]
 <2> The JobIds to remove from the calendar
 
 [id="{upid}-{api}-response"]
-====  Delete Calendar Response
+====  Delete Calendar Job Response
 
 The returned +{response}+ contains the updated Calendar:
 
diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc
index e23967bfe30..02158673da2 100644
--- a/docs/java-rest/high-level/supported-apis.asciidoc
+++ b/docs/java-rest/high-level/supported-apis.asciidoc
@@ -268,6 +268,7 @@ The Java High Level REST Client supports the following Machine Learning APIs:
 * <<{upid}-put-calendar>>
 * <<{upid}-get-calendar-events>>
 * <<{upid}-post-calendar-event>>
+* <<{upid}-delete-calendar-event>>
 * <<{upid}-put-calendar-job>>
 * <<{upid}-delete-calendar-job>>
 * <<{upid}-delete-calendar>>
@@ -308,6 +309,7 @@ include::ml/get-calendars.asciidoc[]
 include::ml/put-calendar.asciidoc[]
 include::ml/get-calendar-events.asciidoc[]
 include::ml/post-calendar-event.asciidoc[]
+include::ml/delete-calendar-event.asciidoc[]
 include::ml/put-calendar-job.asciidoc[]
 include::ml/delete-calendar-job.asciidoc[]
 include::ml/delete-calendar.asciidoc[]