Browse Source

HLRC ML Add Event To Calendar API (#35704)

* HLRC: ML Adding Post event to calendar api

* Fixing tests and serialization

* removing unused import
Benjamin Trent 7 years ago
parent
commit
7657e6d274

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

@@ -50,6 +50,7 @@ import org.elasticsearch.client.ml.GetModelSnapshotsRequest;
 import org.elasticsearch.client.ml.GetOverallBucketsRequest;
 import org.elasticsearch.client.ml.GetRecordsRequest;
 import org.elasticsearch.client.ml.OpenJobRequest;
+import org.elasticsearch.client.ml.PostCalendarEventRequest;
 import org.elasticsearch.client.ml.PostDataRequest;
 import org.elasticsearch.client.ml.PreviewDatafeedRequest;
 import org.elasticsearch.client.ml.PutCalendarJobRequest;
@@ -538,6 +539,21 @@ final class MLRequestConverters {
         return request;
     }
 
+    static Request postCalendarEvents(PostCalendarEventRequest postCalendarEventRequest) throws IOException {
+        String endpoint = new EndpointBuilder()
+            .addPathPartAsIs("_xpack")
+            .addPathPartAsIs("ml")
+            .addPathPartAsIs("calendars")
+            .addPathPart(postCalendarEventRequest.getCalendarId())
+            .addPathPartAsIs("events")
+            .build();
+        Request request = new Request(HttpPost.METHOD_NAME, endpoint);
+        request.setEntity(createEntity(postCalendarEventRequest,
+            REQUEST_BODY_CONTENT_TYPE,
+            PostCalendarEventRequest.EXCLUDE_CALENDAR_ID_PARAMS));
+        return request;
+    }
+
     static Request putFilter(PutFilterRequest putFilterRequest) throws IOException {
         String endpoint = new EndpointBuilder()
             .addPathPartAsIs("_xpack")

+ 43 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java

@@ -60,6 +60,8 @@ import org.elasticsearch.client.ml.GetRecordsRequest;
 import org.elasticsearch.client.ml.GetRecordsResponse;
 import org.elasticsearch.client.ml.OpenJobRequest;
 import org.elasticsearch.client.ml.OpenJobResponse;
+import org.elasticsearch.client.ml.PostCalendarEventRequest;
+import org.elasticsearch.client.ml.PostCalendarEventResponse;
 import org.elasticsearch.client.ml.PostDataRequest;
 import org.elasticsearch.client.ml.PostDataResponse;
 import org.elasticsearch.client.ml.PreviewDatafeedRequest;
@@ -1384,6 +1386,47 @@ public final class MachineLearningClient {
                 Collections.emptySet());
     }
 
+    /**
+     * Creates new events for a a machine learning calendar
+     * <p>
+     * For additional info
+     * see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-post-calendar-event.html">
+     *  Add Events to Calendar API</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 PostCalendarEventRequest} containing the scheduled events
+     * @throws IOException when there is a serialization issue sending the request or receiving the response
+     */
+    public PostCalendarEventResponse postCalendarEvent(PostCalendarEventRequest request, RequestOptions options) throws IOException {
+        return restHighLevelClient.performRequestAndParseEntity(request,
+            MLRequestConverters::postCalendarEvents,
+            options,
+            PostCalendarEventResponse::fromXContent,
+            Collections.emptySet());
+    }
+
+    /**
+     * Creates new events for a a machine learning calendar asynchronously, notifies the listener on completion
+     * <p>
+     * For additional info
+     * see <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/ml-post-calendar-event.html">
+     *  Add Events to Calendar API</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 postCalendarEventAsync(PostCalendarEventRequest request, RequestOptions options,
+                                       ActionListener<PostCalendarEventResponse> listener) {
+        restHighLevelClient.performRequestAsyncAndParseEntity(request,
+            MLRequestConverters::postCalendarEvents,
+            options,
+            PostCalendarEventResponse::fromXContent,
+            listener,
+            Collections.emptySet());
+    }
+
     /**
      * Creates a new Machine Learning Filter
      * <p>

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

@@ -652,7 +652,12 @@ final class RequestConverters {
     }
 
     static HttpEntity createEntity(ToXContent toXContent, XContentType xContentType) throws IOException {
-        BytesRef source = XContentHelper.toXContent(toXContent, xContentType, false).toBytesRef();
+        return createEntity(toXContent, xContentType, ToXContent.EMPTY_PARAMS);
+    }
+
+    static HttpEntity createEntity(ToXContent toXContent, XContentType xContentType, ToXContent.Params toXContentParams)
+        throws IOException {
+        BytesRef source = XContentHelper.toXContent(toXContent, xContentType, toXContentParams, false).toBytesRef();
         return new ByteArrayEntity(source.bytes, source.offset, source.length, createContentType(xContentType));
     }
 

+ 113 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/ml/PostCalendarEventRequest.java

@@ -0,0 +1,113 @@
+/*
+ * 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 org.elasticsearch.client.ml.calendars.Calendar;
+import org.elasticsearch.client.ml.calendars.ScheduledEvent;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Request to add a ScheduledEvent to a Machine Learning calendar
+ */
+public class PostCalendarEventRequest extends ActionRequest implements ToXContentObject {
+
+    private final String calendarId;
+    private final List<ScheduledEvent> scheduledEvents;
+
+    public static final String INCLUDE_CALENDAR_ID_KEY = "include_calendar_id";
+    public static final ParseField EVENTS = new ParseField("events");
+
+    @SuppressWarnings("unchecked")
+    public static final ConstructingObjectParser<PostCalendarEventRequest, Void> PARSER =
+        new ConstructingObjectParser<>("post_calendar_event_request",
+            a -> new PostCalendarEventRequest((String)a[0], (List<ScheduledEvent>)a[1]));
+
+    static {
+        PARSER.declareString(ConstructingObjectParser.constructorArg(), Calendar.ID);
+        PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(),
+            (p, c) -> ScheduledEvent.PARSER.apply(p, null), EVENTS);
+    }
+    public static final MapParams EXCLUDE_CALENDAR_ID_PARAMS =
+        new MapParams(Collections.singletonMap(INCLUDE_CALENDAR_ID_KEY, Boolean.toString(false)));
+
+    /**
+     * Create a new PostCalendarEventRequest with an existing non-null calendarId and a list of Scheduled events
+     *
+     * @param calendarId The ID of the calendar, must be non-null
+     * @param scheduledEvents The non-null, non-empty, list of {@link ScheduledEvent} objects to add to the calendar
+     */
+    public PostCalendarEventRequest(String calendarId, List<ScheduledEvent> scheduledEvents) {
+        this.calendarId = Objects.requireNonNull(calendarId, "[calendar_id] must not be null.");
+        this.scheduledEvents = Objects.requireNonNull(scheduledEvents, "[events] must not be null.");
+        if (scheduledEvents.isEmpty()) {
+            throw new IllegalArgumentException("At least 1 event is required");
+        }
+    }
+
+    public String getCalendarId() {
+        return calendarId;
+    }
+
+    public List<ScheduledEvent> getScheduledEvents() {
+        return scheduledEvents;
+    }
+
+    @Override
+    public ActionRequestValidationException validate() {
+        return null;
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        builder.startObject();
+        if (params.paramAsBoolean(INCLUDE_CALENDAR_ID_KEY, true)) {
+            builder.field(Calendar.ID.getPreferredName(), calendarId);
+        }
+        builder.field(EVENTS.getPreferredName(), scheduledEvents);
+        builder.endObject();
+        return builder;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(calendarId, scheduledEvents);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        PostCalendarEventRequest other = (PostCalendarEventRequest) obj;
+        return Objects.equals(calendarId, other.calendarId) && Objects.equals(scheduledEvents, other.scheduledEvents);
+    }
+}

+ 93 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/ml/PostCalendarEventResponse.java

@@ -0,0 +1,93 @@
+/*
+ * 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.ActionResponse;
+import org.elasticsearch.client.ml.calendars.ScheduledEvent;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Response to adding ScheduledEvent(s) to a Machine Learning calendar
+ */
+public class PostCalendarEventResponse extends ActionResponse implements ToXContentObject {
+
+    private final List<ScheduledEvent> scheduledEvents;
+    public static final ParseField EVENTS = new ParseField("events");
+
+    @SuppressWarnings("unchecked")
+    public static final ConstructingObjectParser<PostCalendarEventResponse, Void> PARSER =
+        new ConstructingObjectParser<>("post_calendar_event_response",
+            true,
+            a -> new PostCalendarEventResponse((List<ScheduledEvent>)a[0]));
+
+    static {
+        PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(),
+            (p, c) -> ScheduledEvent.PARSER.apply(p, null), EVENTS);
+    }
+
+    public static PostCalendarEventResponse fromXContent(XContentParser parser) throws IOException {
+        return PARSER.parse(parser, null);
+    }
+
+    /**
+     * Create a new PostCalendarEventResponse containing the scheduled Events
+     *
+     * @param scheduledEvents The list of {@link ScheduledEvent} objects
+     */
+    public PostCalendarEventResponse(List<ScheduledEvent> scheduledEvents) {
+        this.scheduledEvents = scheduledEvents;
+    }
+
+    public List<ScheduledEvent> getScheduledEvents() {
+        return scheduledEvents;
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        builder.startObject();
+        builder.field(EVENTS.getPreferredName(), scheduledEvents);
+        builder.endObject();
+        return builder;
+    }
+
+    @Override
+    public int hashCode(){
+        return Objects.hash(scheduledEvents);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        PostCalendarEventResponse other = (PostCalendarEventResponse) obj;
+        return Objects.equals(scheduledEvents, other.scheduledEvents);
+    }
+}

+ 21 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java

@@ -46,6 +46,7 @@ import org.elasticsearch.client.ml.GetModelSnapshotsRequest;
 import org.elasticsearch.client.ml.GetOverallBucketsRequest;
 import org.elasticsearch.client.ml.GetRecordsRequest;
 import org.elasticsearch.client.ml.OpenJobRequest;
+import org.elasticsearch.client.ml.PostCalendarEventRequest;
 import org.elasticsearch.client.ml.PostDataRequest;
 import org.elasticsearch.client.ml.PreviewDatafeedRequest;
 import org.elasticsearch.client.ml.PutCalendarJobRequest;
@@ -61,6 +62,8 @@ import org.elasticsearch.client.ml.UpdateJobRequest;
 import org.elasticsearch.client.ml.UpdateModelSnapshotRequest;
 import org.elasticsearch.client.ml.calendars.Calendar;
 import org.elasticsearch.client.ml.calendars.CalendarTests;
+import org.elasticsearch.client.ml.calendars.ScheduledEvent;
+import org.elasticsearch.client.ml.calendars.ScheduledEventTests;
 import org.elasticsearch.client.ml.datafeed.DatafeedConfig;
 import org.elasticsearch.client.ml.datafeed.DatafeedConfigTests;
 import org.elasticsearch.client.ml.job.config.AnalysisConfig;
@@ -73,6 +76,7 @@ import org.elasticsearch.client.ml.job.config.MlFilterTests;
 import org.elasticsearch.client.ml.job.util.PageParams;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentParser;
 import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.common.xcontent.json.JsonXContent;
@@ -83,6 +87,7 @@ import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import static org.hamcrest.Matchers.equalTo;
@@ -586,6 +591,22 @@ public class MLRequestConvertersTests extends ESTestCase {
         assertEquals("/_xpack/ml/calendars/" + deleteCalendarRequest.getCalendarId(), request.getEndpoint());
     }
 
+    public void testPostCalendarEvent() throws Exception {
+        String calendarId = randomAlphaOfLength(10);
+        List<ScheduledEvent> events = Arrays.asList(ScheduledEventTests.testInstance(),
+            ScheduledEventTests.testInstance(),
+            ScheduledEventTests.testInstance());
+        PostCalendarEventRequest postCalendarEventRequest = new PostCalendarEventRequest(calendarId, events);
+
+        Request request = MLRequestConverters.postCalendarEvents(postCalendarEventRequest);
+        assertEquals(HttpPost.METHOD_NAME, request.getMethod());
+        assertEquals("/_xpack/ml/calendars/" + calendarId + "/events", request.getEndpoint());
+
+        XContentBuilder builder = JsonXContent.contentBuilder();
+        builder = postCalendarEventRequest.toXContent(builder, PostCalendarEventRequest.EXCLUDE_CALENDAR_ID_PARAMS);
+        assertEquals(Strings.toString(builder), requestEntityToString(request));
+    }
+
     public void testPutFilter() throws IOException {
         MlFilter filter = MlFilterTests.createRandomBuilder("foo").build();
         PutFilterRequest putFilterRequest = new PutFilterRequest(filter);

+ 22 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java

@@ -57,6 +57,8 @@ import org.elasticsearch.client.ml.GetModelSnapshotsRequest;
 import org.elasticsearch.client.ml.GetModelSnapshotsResponse;
 import org.elasticsearch.client.ml.OpenJobRequest;
 import org.elasticsearch.client.ml.OpenJobResponse;
+import org.elasticsearch.client.ml.PostCalendarEventRequest;
+import org.elasticsearch.client.ml.PostCalendarEventResponse;
 import org.elasticsearch.client.ml.PostDataRequest;
 import org.elasticsearch.client.ml.PostDataResponse;
 import org.elasticsearch.client.ml.PreviewDatafeedRequest;
@@ -81,6 +83,8 @@ import org.elasticsearch.client.ml.UpdateModelSnapshotRequest;
 import org.elasticsearch.client.ml.UpdateModelSnapshotResponse;
 import org.elasticsearch.client.ml.calendars.Calendar;
 import org.elasticsearch.client.ml.calendars.CalendarTests;
+import org.elasticsearch.client.ml.calendars.ScheduledEvent;
+import org.elasticsearch.client.ml.calendars.ScheduledEventTests;
 import org.elasticsearch.client.ml.datafeed.DatafeedConfig;
 import org.elasticsearch.client.ml.datafeed.DatafeedState;
 import org.elasticsearch.client.ml.datafeed.DatafeedStats;
@@ -917,6 +921,24 @@ public class MachineLearningIT extends ESRestHighLevelClientTestCase {
         assertThat(exception.status().getStatus(), equalTo(404));
     }
 
+    public void testPostCalendarEvent() throws Exception {
+        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));
+        }
+
+        PostCalendarEventRequest postCalendarEventRequest = new PostCalendarEventRequest(calendar.getId(), events);
+
+        PostCalendarEventResponse postCalendarEventResponse = execute(postCalendarEventRequest,
+            machineLearningClient::postCalendarEvent,
+            machineLearningClient::postCalendarEventAsync);
+        assertThat(postCalendarEventResponse.getScheduledEvents(), containsInAnyOrder(events.toArray()));
+    }
+
     public void testPutFilter() throws Exception {
         String filterId = "filter-job-test";
         MlFilter mlFilter = MlFilter.builder(filterId)

+ 59 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java

@@ -73,6 +73,8 @@ import org.elasticsearch.client.ml.GetRecordsRequest;
 import org.elasticsearch.client.ml.GetRecordsResponse;
 import org.elasticsearch.client.ml.OpenJobRequest;
 import org.elasticsearch.client.ml.OpenJobResponse;
+import org.elasticsearch.client.ml.PostCalendarEventRequest;
+import org.elasticsearch.client.ml.PostCalendarEventResponse;
 import org.elasticsearch.client.ml.PostDataRequest;
 import org.elasticsearch.client.ml.PostDataResponse;
 import org.elasticsearch.client.ml.PreviewDatafeedRequest;
@@ -96,6 +98,8 @@ import org.elasticsearch.client.ml.UpdateJobRequest;
 import org.elasticsearch.client.ml.UpdateModelSnapshotRequest;
 import org.elasticsearch.client.ml.UpdateModelSnapshotResponse;
 import org.elasticsearch.client.ml.calendars.Calendar;
+import org.elasticsearch.client.ml.calendars.ScheduledEvent;
+import org.elasticsearch.client.ml.calendars.ScheduledEventTests;
 import org.elasticsearch.client.ml.datafeed.ChunkingConfig;
 import org.elasticsearch.client.ml.datafeed.DatafeedConfig;
 import org.elasticsearch.client.ml.datafeed.DatafeedStats;
@@ -2377,6 +2381,61 @@ public class MlClientDocumentationIT extends ESRestHighLevelClientTestCase {
         assertTrue(latch.await(30L, TimeUnit.SECONDS));
     }
 
+    public void testPostCalendarEvent() throws IOException, InterruptedException {
+        RestHighLevelClient client = highLevelClient();
+
+        Calendar calendar = new Calendar("holidays", Collections.singletonList("job_1"), "A calendar for public holidays");
+        PutCalendarRequest putRequest = new PutCalendarRequest(calendar);
+        client.machineLearning().putCalendar(putRequest, RequestOptions.DEFAULT);
+        {
+            List<ScheduledEvent> events = Collections.singletonList(ScheduledEventTests.testInstance(calendar.getId(), null));
+
+            // tag::post-calendar-event-request
+            PostCalendarEventRequest request = new PostCalendarEventRequest("holidays", // <1>
+                events); // <2>
+            // end::post-calendar-event-request
+
+            // tag::post-calendar-event-execute
+            PostCalendarEventResponse response = client.machineLearning().postCalendarEvent(request, RequestOptions.DEFAULT);
+            // end::post-calendar-event-execute
+
+            // tag::post-calendar-event-response
+            List<ScheduledEvent> scheduledEvents = response.getScheduledEvents(); // <1>
+            // end::post-calendar-event-response
+
+            assertEquals(1, scheduledEvents.size());
+        }
+        {
+            List<ScheduledEvent> events = Collections.singletonList(ScheduledEventTests.testInstance());
+            PostCalendarEventRequest request = new PostCalendarEventRequest("holidays", events); // <1>
+
+            // tag::post-calendar-event-execute-listener
+            ActionListener<PostCalendarEventResponse> listener =
+                new ActionListener<PostCalendarEventResponse>() {
+                    @Override
+                    public void onResponse(PostCalendarEventResponse postCalendarsResponse) {
+                        // <1>
+                    }
+
+                    @Override
+                    public void onFailure(Exception e) {
+                        // <2>
+                    }
+                };
+            // end::post-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::post-calendar-event-execute-async
+            client.machineLearning().postCalendarEventAsync(request, RequestOptions.DEFAULT, listener); // <1>
+            // end::post-calendar-event-execute-async
+
+            assertTrue(latch.await(30L, TimeUnit.SECONDS));
+        }
+    }
+    
     public void testCreateFilter() throws Exception {
         RestHighLevelClient client = highLevelClient();
         {

+ 53 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/ml/PostCalendarEventRequestTests.java

@@ -0,0 +1,53 @@
+/*
+ * 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.client.ml.calendars.ScheduledEvent;
+import org.elasticsearch.client.ml.calendars.ScheduledEventTests;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.test.AbstractXContentTestCase;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class PostCalendarEventRequestTests extends AbstractXContentTestCase<PostCalendarEventRequest> {
+
+    @Override
+    protected PostCalendarEventRequest createTestInstance() {
+        String calendarId = randomAlphaOfLength(10);
+        int numberOfEvents = randomIntBetween(1, 10);
+        List<ScheduledEvent> events = new ArrayList<>(numberOfEvents);
+        for (int i = 0; i < numberOfEvents; i++) {
+            events.add(ScheduledEventTests.testInstance());
+        }
+        return new PostCalendarEventRequest(calendarId, events);
+    }
+
+    @Override
+    protected PostCalendarEventRequest doParseInstance(XContentParser parser) throws IOException {
+        return PostCalendarEventRequest.PARSER.apply(parser, null);
+    }
+
+    @Override
+    protected boolean supportsUnknownFields() {
+        return false;
+    }
+}

+ 51 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/ml/PostCalendarEventResponseTests.java

@@ -0,0 +1,51 @@
+/*
+ * 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.client.ml.calendars.ScheduledEvent;
+import org.elasticsearch.client.ml.calendars.ScheduledEventTests;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.test.AbstractXContentTestCase;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class PostCalendarEventResponseTests extends AbstractXContentTestCase<PostCalendarEventResponse> {
+    @Override
+    protected PostCalendarEventResponse createTestInstance() {
+        int numberOfEvents = randomIntBetween(1, 10);
+        List<ScheduledEvent> events = new ArrayList<>(numberOfEvents);
+        for (int i = 0; i < numberOfEvents; i++) {
+            events.add(ScheduledEventTests.testInstance());
+        }
+        return new PostCalendarEventResponse(events);
+    }
+
+    @Override
+    protected PostCalendarEventResponse doParseInstance(XContentParser parser) throws IOException {
+        return PostCalendarEventResponse.fromXContent(parser);
+    }
+
+    @Override
+    protected boolean supportsUnknownFields() {
+        return true;
+    }
+}

+ 8 - 3
client/rest-high-level/src/test/java/org/elasticsearch/client/ml/calendars/ScheduledEventTests.java

@@ -19,6 +19,7 @@
 
 package org.elasticsearch.client.ml.calendars;
 
+import org.elasticsearch.common.Nullable;
 import org.elasticsearch.common.xcontent.XContentParser;
 import org.elasticsearch.test.AbstractXContentTestCase;
 
@@ -26,12 +27,16 @@ import java.util.Date;
 
 public class ScheduledEventTests extends AbstractXContentTestCase<ScheduledEvent> {
 
-    public static ScheduledEvent testInstance() {
+    public static ScheduledEvent testInstance(String calendarId, @Nullable String eventId) {
         Date start = new Date(randomNonNegativeLong());
         Date end = new Date(start.getTime() + randomIntBetween(1, 10000) * 1000);
 
-        return new ScheduledEvent(randomAlphaOfLength(10), start, end, randomAlphaOfLengthBetween(1, 20),
-                randomBoolean() ? null : randomAlphaOfLength(7));
+        return new ScheduledEvent(randomAlphaOfLength(10), start, end, calendarId, eventId);
+    }
+
+    public static ScheduledEvent testInstance() {
+        return testInstance(randomAlphaOfLengthBetween(1, 20),
+            randomBoolean() ? null : randomAlphaOfLength(7));
     }
 
     @Override

+ 38 - 0
docs/java-rest/high-level/ml/post-calendar-event.asciidoc

@@ -0,0 +1,38 @@
+--
+:api: post-calendar-event
+:request: PostCalendarEventRequest
+:response: PostCalendarEventResponse
+--
+[id="{upid}-{api}"]
+=== Post Calendar Event API
+Adds new ScheduledEvents to an existing {ml} calendar.
+
+The API accepts a +{request}+ and responds
+with a +{response}+ object.
+
+[id="{upid}-{api}-request"]
+==== Post Calendar Event Request
+
+A +{request}+ is constructed with a calendar ID object
+and a non-empty list of scheduled events.
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[{api}-request]
+--------------------------------------------------
+<1> Non-null existing calendar ID
+<2> Non-null, non-empty collection of `ScheduledEvent` objects
+
+
+[id="{upid}-{api}-response"]
+==== Post Calendar Event Response
+
+The returned +{response}+ contains the added `ScheduledEvent` objects:
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[{api}-response]
+--------------------------------------------------
+<1> The `ScheduledEvent` objects that were added to the calendar
+
+include::../execution.asciidoc[]

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

@@ -266,6 +266,7 @@ The Java High Level REST Client supports the following Machine Learning APIs:
 * <<{upid}-get-categories>>
 * <<{upid}-get-calendars>>
 * <<{upid}-put-calendar>>
+* <<{upid}-post-calendar-event>>
 * <<{upid}-put-calendar-job>>
 * <<{upid}-delete-calendar-job>>
 * <<{upid}-delete-calendar>>
@@ -303,6 +304,7 @@ include::ml/get-influencers.asciidoc[]
 include::ml/get-categories.asciidoc[]
 include::ml/get-calendars.asciidoc[]
 include::ml/put-calendar.asciidoc[]
+include::ml/post-calendar-event.asciidoc[]
 include::ml/put-calendar-job.asciidoc[]
 include::ml/delete-calendar-job.asciidoc[]
 include::ml/delete-calendar.asciidoc[]