diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/ActionStatus.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/ActionStatus.java new file mode 100644 index 00000000000..ec413c10fa7 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/ActionStatus.java @@ -0,0 +1,341 @@ +/* + * 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.watcher; + +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.XContentParser; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; + +import java.io.IOException; +import java.util.Locale; +import java.util.Objects; + +public class ActionStatus { + + private final AckStatus ackStatus; + @Nullable private final Execution lastExecution; + @Nullable private final Execution lastSuccessfulExecution; + @Nullable private final Throttle lastThrottle; + + public ActionStatus(AckStatus ackStatus, + @Nullable Execution lastExecution, + @Nullable Execution lastSuccessfulExecution, + @Nullable Throttle lastThrottle) { + this.ackStatus = ackStatus; + this.lastExecution = lastExecution; + this.lastSuccessfulExecution = lastSuccessfulExecution; + this.lastThrottle = lastThrottle; + } + + public AckStatus ackStatus() { + return ackStatus; + } + + public Execution lastExecution() { + return lastExecution; + } + + public Execution lastSuccessfulExecution() { + return lastSuccessfulExecution; + } + + public Throttle lastThrottle() { + return lastThrottle; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ActionStatus that = (ActionStatus) o; + + return Objects.equals(ackStatus, that.ackStatus) && + Objects.equals(lastExecution, that.lastExecution) && + Objects.equals(lastSuccessfulExecution, that.lastSuccessfulExecution) && + Objects.equals(lastThrottle, that.lastThrottle); + } + + @Override + public int hashCode() { + return Objects.hash(ackStatus, lastExecution, lastSuccessfulExecution, lastThrottle); + } + + public static ActionStatus parse(String actionId, XContentParser parser) throws IOException { + AckStatus ackStatus = null; + Execution lastExecution = null; + Execution lastSuccessfulExecution = null; + Throttle lastThrottle = null; + + String currentFieldName = null; + XContentParser.Token token; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (Field.ACK_STATUS.match(currentFieldName, parser.getDeprecationHandler())) { + ackStatus = AckStatus.parse(actionId, parser); + } else if (Field.LAST_EXECUTION.match(currentFieldName, parser.getDeprecationHandler())) { + lastExecution = Execution.parse(actionId, parser); + } else if (Field.LAST_SUCCESSFUL_EXECUTION.match(currentFieldName, parser.getDeprecationHandler())) { + lastSuccessfulExecution = Execution.parse(actionId, parser); + } else if (Field.LAST_THROTTLE.match(currentFieldName, parser.getDeprecationHandler())) { + lastThrottle = Throttle.parse(actionId, parser); + } else { + parser.skipChildren(); + } + } + if (ackStatus == null) { + throw new ElasticsearchParseException("could not parse action status for [{}]. missing required field [{}]", + actionId, Field.ACK_STATUS.getPreferredName()); + } + return new ActionStatus(ackStatus, lastExecution, lastSuccessfulExecution, lastThrottle); + } + + public static class AckStatus { + + public enum State { + AWAITS_SUCCESSFUL_EXECUTION, + ACKABLE, + ACKED; + } + + private final DateTime timestamp; + private final State state; + + public AckStatus(DateTime timestamp, State state) { + this.timestamp = timestamp.toDateTime(DateTimeZone.UTC); + this.state = state; + } + + public DateTime timestamp() { + return timestamp; + } + + public State state() { + return state; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + AckStatus ackStatus = (AckStatus) o; + + return Objects.equals(timestamp, ackStatus.timestamp) && Objects.equals(state, ackStatus.state); + } + + @Override + public int hashCode() { + return Objects.hash(timestamp, state); + } + + public static AckStatus parse(String actionId, XContentParser parser) throws IOException { + DateTime timestamp = null; + State state = null; + + String currentFieldName = null; + XContentParser.Token token; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (Field.TIMESTAMP.match(currentFieldName, parser.getDeprecationHandler())) { + timestamp = WatchStatusDateParser.parseDate(parser.text()); + } else if (Field.ACK_STATUS_STATE.match(currentFieldName, parser.getDeprecationHandler())) { + state = State.valueOf(parser.text().toUpperCase(Locale.ROOT)); + } else { + parser.skipChildren(); + } + } + if (timestamp == null) { + throw new ElasticsearchParseException("could not parse action status for [{}]. missing required field [{}.{}]", + actionId, Field.ACK_STATUS.getPreferredName(), Field.TIMESTAMP.getPreferredName()); + } + if (state == null) { + throw new ElasticsearchParseException("could not parse action status for [{}]. missing required field [{}.{}]", + actionId, Field.ACK_STATUS.getPreferredName(), Field.ACK_STATUS_STATE.getPreferredName()); + } + return new AckStatus(timestamp, state); + } + } + + public static class Execution { + + public static Execution successful(DateTime timestamp) { + return new Execution(timestamp, true, null); + } + + public static Execution failure(DateTime timestamp, String reason) { + return new Execution(timestamp, false, reason); + } + + private final DateTime timestamp; + private final boolean successful; + private final String reason; + + private Execution(DateTime timestamp, boolean successful, String reason) { + this.timestamp = timestamp.toDateTime(DateTimeZone.UTC); + this.successful = successful; + this.reason = reason; + } + + public DateTime timestamp() { + return timestamp; + } + + public boolean successful() { + return successful; + } + + public String reason() { + return reason; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Execution execution = (Execution) o; + + return Objects.equals(successful, execution.successful) && + Objects.equals(timestamp, execution.timestamp) && + Objects.equals(reason, execution.reason); + } + + @Override + public int hashCode() { + return Objects.hash(timestamp, successful, reason); + } + + public static Execution parse(String actionId, XContentParser parser) throws IOException { + DateTime timestamp = null; + Boolean successful = null; + String reason = null; + + String currentFieldName = null; + XContentParser.Token token; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (Field.TIMESTAMP.match(currentFieldName, parser.getDeprecationHandler())) { + timestamp = WatchStatusDateParser.parseDate(parser.text()); + } else if (Field.EXECUTION_SUCCESSFUL.match(currentFieldName, parser.getDeprecationHandler())) { + successful = parser.booleanValue(); + } else if (Field.REASON.match(currentFieldName, parser.getDeprecationHandler())) { + reason = parser.text(); + } else { + parser.skipChildren(); + } + } + if (timestamp == null) { + throw new ElasticsearchParseException("could not parse action status for [{}]. missing required field [{}.{}]", + actionId, Field.LAST_EXECUTION.getPreferredName(), Field.TIMESTAMP.getPreferredName()); + } + if (successful == null) { + throw new ElasticsearchParseException("could not parse action status for [{}]. missing required field [{}.{}]", + actionId, Field.LAST_EXECUTION.getPreferredName(), Field.EXECUTION_SUCCESSFUL.getPreferredName()); + } + if (successful) { + return successful(timestamp); + } + if (reason == null) { + throw new ElasticsearchParseException("could not parse action status for [{}]. missing required field for unsuccessful" + + " execution [{}.{}]", actionId, Field.LAST_EXECUTION.getPreferredName(), Field.REASON.getPreferredName()); + } + return failure(timestamp, reason); + } + } + + public static class Throttle { + + private final DateTime timestamp; + private final String reason; + + public Throttle(DateTime timestamp, String reason) { + this.timestamp = timestamp.toDateTime(DateTimeZone.UTC); + this.reason = reason; + } + + public DateTime timestamp() { + return timestamp; + } + + public String reason() { + return reason; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Throttle throttle = (Throttle) o; + return Objects.equals(timestamp, throttle.timestamp) && Objects.equals(reason, throttle.reason); + } + + @Override + public int hashCode() { + return Objects.hash(timestamp, reason); + } + + public static Throttle parse(String actionId, XContentParser parser) throws IOException { + DateTime timestamp = null; + String reason = null; + + String currentFieldName = null; + XContentParser.Token token; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (Field.TIMESTAMP.match(currentFieldName, parser.getDeprecationHandler())) { + timestamp = WatchStatusDateParser.parseDate(parser.text()); + } else if (Field.REASON.match(currentFieldName, parser.getDeprecationHandler())) { + reason = parser.text(); + } else { + parser.skipChildren(); + } + } + if (timestamp == null) { + throw new ElasticsearchParseException("could not parse action status for [{}]. missing required field [{}.{}]", + actionId, Field.LAST_THROTTLE.getPreferredName(), Field.TIMESTAMP.getPreferredName()); + } + if (reason == null) { + throw new ElasticsearchParseException("could not parse action status for [{}]. missing required field [{}.{}]", + actionId, Field.LAST_THROTTLE.getPreferredName(), Field.REASON.getPreferredName()); + } + return new Throttle(timestamp, reason); + } + } + + private interface Field { + ParseField ACK_STATUS = new ParseField("ack"); + ParseField ACK_STATUS_STATE = new ParseField("state"); + ParseField LAST_EXECUTION = new ParseField("last_execution"); + ParseField LAST_SUCCESSFUL_EXECUTION = new ParseField("last_successful_execution"); + ParseField EXECUTION_SUCCESSFUL = new ParseField("successful"); + ParseField LAST_THROTTLE = new ParseField("last_throttle"); + ParseField TIMESTAMP = new ParseField("timestamp"); + ParseField REASON = new ParseField("reason"); + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/ExecutionState.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/ExecutionState.java new file mode 100644 index 00000000000..001745825d0 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/ExecutionState.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.client.watcher; + +import java.util.Locale; + +public enum ExecutionState { + + // the condition of the watch was not met + EXECUTION_NOT_NEEDED, + + // Execution has been throttled due to time-based throttling - this might only affect a single action though + THROTTLED, + + // Execution has been throttled due to ack-based throttling/muting of an action - this might only affect a single action though + ACKNOWLEDGED, + + // regular execution + EXECUTED, + + // an error in the condition or the execution of the input + FAILED, + + // a rejection due to a filled up threadpool + THREADPOOL_REJECTION, + + // the execution was scheduled, but in between the watch was deleted + NOT_EXECUTED_WATCH_MISSING, + + // even though the execution was scheduled, it was not executed, because the watch was already queued in the thread pool + NOT_EXECUTED_ALREADY_QUEUED, + + // this can happen when a watch was executed, but not completely finished (the triggered watch entry was not deleted), and then + // watcher is restarted (manually or due to host switch) - the triggered watch will be executed but the history entry already + // exists + EXECUTED_MULTIPLE_TIMES; + + public String id() { + return name().toLowerCase(Locale.ROOT); + } + + public static ExecutionState resolve(String id) { + return valueOf(id.toUpperCase(Locale.ROOT)); + } + + @Override + public String toString() { + return id(); + } + +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/WatchStatus.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/WatchStatus.java new file mode 100644 index 00000000000..04b747c0363 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/WatchStatus.java @@ -0,0 +1,233 @@ +/* + * 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.watcher; + +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.XContentParser; +import org.joda.time.DateTime; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import static java.util.Collections.emptyMap; +import static java.util.Collections.unmodifiableMap; +import static org.elasticsearch.client.watcher.WatchStatusDateParser.parseDate; +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; +import static org.joda.time.DateTimeZone.UTC; + +public class WatchStatus { + + private final State state; + + private final ExecutionState executionState; + private final DateTime lastChecked; + private final DateTime lastMetCondition; + private final long version; + private final Map actions; + + public WatchStatus(long version, + State state, + ExecutionState executionState, + DateTime lastChecked, + DateTime lastMetCondition, + Map actions) { + this.version = version; + this.lastChecked = lastChecked; + this.lastMetCondition = lastMetCondition; + this.actions = actions; + this.state = state; + this.executionState = executionState; + } + + public State state() { + return state; + } + + public boolean checked() { + return lastChecked != null; + } + + public DateTime lastChecked() { + return lastChecked; + } + + public DateTime lastMetCondition() { + return lastMetCondition; + } + + public ActionStatus actionStatus(String actionId) { + return actions.get(actionId); + } + + public long version() { + return version; + } + + public ExecutionState getExecutionState() { + return executionState; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + WatchStatus that = (WatchStatus) o; + + return Objects.equals(lastChecked, that.lastChecked) && + Objects.equals(lastMetCondition, that.lastMetCondition) && + Objects.equals(version, that.version) && + Objects.equals(executionState, that.executionState) && + Objects.equals(actions, that.actions); + } + + @Override + public int hashCode() { + return Objects.hash(lastChecked, lastMetCondition, actions, version, executionState); + } + + public static WatchStatus parse(XContentParser parser) throws IOException { + State state = null; + ExecutionState executionState = null; + DateTime lastChecked = null; + DateTime lastMetCondition = null; + Map actions = null; + long version = -1; + + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.currentToken(), parser::getTokenLocation); + + String currentFieldName = null; + XContentParser.Token token; + + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (Field.STATE.match(currentFieldName, parser.getDeprecationHandler())) { + try { + state = State.parse(parser); + } catch (ElasticsearchParseException e) { + throw new ElasticsearchParseException("could not parse watch status. failed to parse field [{}]", + e, currentFieldName); + } + } else if (Field.VERSION.match(currentFieldName, parser.getDeprecationHandler())) { + if (token.isValue()) { + version = parser.longValue(); + } else { + throw new ElasticsearchParseException("could not parse watch status. expecting field [{}] to hold a long " + + "value, found [{}] instead", currentFieldName, token); + } + } else if (Field.LAST_CHECKED.match(currentFieldName, parser.getDeprecationHandler())) { + if (token.isValue()) { + lastChecked = parseDate(currentFieldName, parser); + } else { + throw new ElasticsearchParseException("could not parse watch status. expecting field [{}] to hold a date " + + "value, found [{}] instead", currentFieldName, token); + } + } else if (Field.LAST_MET_CONDITION.match(currentFieldName, parser.getDeprecationHandler())) { + if (token.isValue()) { + lastMetCondition = parseDate(currentFieldName, parser); + } else { + throw new ElasticsearchParseException("could not parse watch status. expecting field [{}] to hold a date " + + "value, found [{}] instead", currentFieldName, token); + } + } else if (Field.EXECUTION_STATE.match(currentFieldName, parser.getDeprecationHandler())) { + if (token.isValue()) { + executionState = ExecutionState.resolve(parser.text()); + } else { + throw new ElasticsearchParseException("could not parse watch status. expecting field [{}] to hold a string " + + "value, found [{}] instead", currentFieldName, token); + } + } else if (Field.ACTIONS.match(currentFieldName, parser.getDeprecationHandler())) { + actions = new HashMap<>(); + if (token == XContentParser.Token.START_OBJECT) { + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else { + ActionStatus actionStatus = ActionStatus.parse(currentFieldName, parser); + actions.put(currentFieldName, actionStatus); + } + } + } else { + throw new ElasticsearchParseException("could not parse watch status. expecting field [{}] to be an object, " + + "found [{}] instead", currentFieldName, token); + } + } else { + parser.skipChildren(); + } + } + + actions = actions == null ? emptyMap() : unmodifiableMap(actions); + return new WatchStatus(version, state, executionState, lastChecked, lastMetCondition, actions); + } + + public static class State { + + private final boolean active; + private final DateTime timestamp; + + public State(boolean active, DateTime timestamp) { + this.active = active; + this.timestamp = timestamp; + } + + public boolean isActive() { + return active; + } + + public DateTime getTimestamp() { + return timestamp; + } + + public static State parse(XContentParser parser) throws IOException { + if (parser.currentToken() != XContentParser.Token.START_OBJECT) { + throw new ElasticsearchParseException("expected an object but found [{}] instead", parser.currentToken()); + } + boolean active = true; + DateTime timestamp = DateTime.now(UTC); + String currentFieldName = null; + XContentParser.Token token; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (Field.ACTIVE.match(currentFieldName, parser.getDeprecationHandler())) { + active = parser.booleanValue(); + } else if (Field.TIMESTAMP.match(currentFieldName, parser.getDeprecationHandler())) { + timestamp = parseDate(currentFieldName, parser); + } + } + return new State(active, timestamp); + } + } + + public interface Field { + ParseField STATE = new ParseField("state"); + ParseField ACTIVE = new ParseField("active"); + ParseField TIMESTAMP = new ParseField("timestamp"); + ParseField LAST_CHECKED = new ParseField("last_checked"); + ParseField LAST_MET_CONDITION = new ParseField("last_met_condition"); + ParseField ACTIONS = new ParseField("actions"); + ParseField VERSION = new ParseField("version"); + ParseField EXECUTION_STATE = new ParseField("execution_state"); + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/WatchStatusDateParser.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/WatchStatusDateParser.java new file mode 100644 index 00000000000..a71ec58ce1c --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/watcher/WatchStatusDateParser.java @@ -0,0 +1,58 @@ +/* + * 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.watcher; + +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.common.joda.FormatDateTimeFormatter; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.mapper.DateFieldMapper; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; + +import java.io.IOException; + +public final class WatchStatusDateParser { + + private static final FormatDateTimeFormatter FORMATTER = DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER; + + private WatchStatusDateParser() { + // Prevent instantiation. + } + + public static DateTime parseDate(String fieldName, XContentParser parser) throws IOException { + XContentParser.Token token = parser.currentToken(); + if (token == XContentParser.Token.VALUE_NUMBER) { + return new DateTime(parser.longValue(), DateTimeZone.UTC); + } + if (token == XContentParser.Token.VALUE_STRING) { + DateTime dateTime = parseDate(parser.text()); + return dateTime.toDateTime(DateTimeZone.UTC); + } + if (token == XContentParser.Token.VALUE_NULL) { + return null; + } + throw new ElasticsearchParseException("could not parse date/time. expected date field [{}] " + + "to be either a number or a string but found [{}] instead", fieldName, token); + } + + public static DateTime parseDate(String text) { + return FORMATTER.parser().parseDateTime(text); + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/watcher/WatchStatusTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/watcher/WatchStatusTests.java new file mode 100644 index 00000000000..cb302b5e028 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/watcher/WatchStatusTests.java @@ -0,0 +1,174 @@ +/* + * 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.watcher; + +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.XContentTestUtils; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; + +import java.io.IOException; +import java.util.function.Predicate; + +public class WatchStatusTests extends ESTestCase { + + public void testBasicParsing() throws IOException { + int expectedVersion = randomIntBetween(0, 100); + ExecutionState expectedExecutionState = randomFrom(ExecutionState.values()); + boolean expectedActive = randomBoolean(); + ActionStatus.AckStatus.State expectedAckState = randomFrom(ActionStatus.AckStatus.State.values()); + + XContentBuilder builder = createTestXContent(expectedVersion, expectedExecutionState, + expectedActive, expectedAckState); + BytesReference bytes = BytesReference.bytes(builder); + + WatchStatus watchStatus = parse(builder.contentType(), bytes); + + assertEquals(expectedVersion, watchStatus.version()); + assertEquals(expectedExecutionState, watchStatus.getExecutionState()); + + assertEquals(new DateTime(1432663467763L, DateTimeZone.UTC), watchStatus.lastChecked()); + assertEquals(DateTime.parse("2015-05-26T18:04:27.763Z"), watchStatus.lastMetCondition()); + + WatchStatus.State watchState = watchStatus.state(); + assertEquals(expectedActive, watchState.isActive()); + assertEquals(DateTime.parse("2015-05-26T18:04:27.723Z"), watchState.getTimestamp()); + + ActionStatus actionStatus = watchStatus.actionStatus("test_index"); + assertNotNull(actionStatus); + + ActionStatus.AckStatus ackStatus = actionStatus.ackStatus(); + assertEquals(DateTime.parse("2015-05-26T18:04:27.763Z"), ackStatus.timestamp()); + assertEquals(expectedAckState, ackStatus.state()); + + ActionStatus.Execution lastExecution = actionStatus.lastExecution(); + assertEquals(DateTime.parse("2015-05-25T18:04:27.733Z"), lastExecution.timestamp()); + assertFalse(lastExecution.successful()); + assertEquals("failed to send email", lastExecution.reason()); + + ActionStatus.Execution lastSuccessfulExecution = actionStatus.lastSuccessfulExecution(); + assertEquals(DateTime.parse("2015-05-25T18:04:27.773Z"), lastSuccessfulExecution.timestamp()); + assertTrue(lastSuccessfulExecution.successful()); + assertNull(lastSuccessfulExecution.reason()); + + ActionStatus.Throttle lastThrottle = actionStatus.lastThrottle(); + assertEquals(DateTime.parse("2015-04-25T18:05:23.445Z"), lastThrottle.timestamp()); + assertEquals("throttling interval is set to [5 seconds] ...", lastThrottle.reason()); + } + + public void testParsingWithUnknownKeys() throws IOException { + int expectedVersion = randomIntBetween(0, 100); + ExecutionState expectedExecutionState = randomFrom(ExecutionState.values()); + boolean expectedActive = randomBoolean(); + ActionStatus.AckStatus.State expectedAckState = randomFrom(ActionStatus.AckStatus.State.values()); + + XContentBuilder builder = createTestXContent(expectedVersion, expectedExecutionState, + expectedActive, expectedAckState); + BytesReference bytes = BytesReference.bytes(builder); + + Predicate excludeFilter = field -> field.equals("actions"); + BytesReference bytesWithRandomFields = XContentTestUtils.insertRandomFields( + builder.contentType(), bytes, excludeFilter, random()); + + WatchStatus watchStatus = parse(builder.contentType(), bytesWithRandomFields); + + assertEquals(expectedVersion, watchStatus.version()); + assertEquals(expectedExecutionState, watchStatus.getExecutionState()); + } + + public void testOptionalFieldsParsing() throws IOException { + XContentType contentType = randomFrom(XContentType.values()); + XContentBuilder builder = XContentFactory.contentBuilder(contentType).startObject() + .field("version", 42) + .startObject("actions") + .startObject("test_index") + .startObject("ack") + .field("timestamp", "2015-05-26T18:04:27.763Z") + .field("state", "ackable") + .endObject() + .startObject("last_execution") + .field("timestamp", "2015-05-25T18:04:27.733Z") + .field("successful", false) + .field("reason", "failed to send email") + .endObject() + .endObject() + .endObject() + .endObject(); + BytesReference bytes = BytesReference.bytes(builder); + + WatchStatus watchStatus = parse(builder.contentType(), bytes); + + assertEquals(42, watchStatus.version()); + assertNull(watchStatus.getExecutionState()); + assertFalse(watchStatus.checked()); + } + + private XContentBuilder createTestXContent(int version, + ExecutionState executionState, + boolean active, + ActionStatus.AckStatus.State ackState) throws IOException { + XContentType contentType = randomFrom(XContentType.values()); + return XContentFactory.contentBuilder(contentType).startObject() + .field("version", version) + .field("execution_state", executionState) + .field("last_checked", 1432663467763L) + .field("last_met_condition", "2015-05-26T18:04:27.763Z") + .startObject("state") + .field("active", active) + .field("timestamp", "2015-05-26T18:04:27.723Z") + .endObject() + .startObject("actions") + .startObject("test_index") + .startObject("ack") + .field("timestamp", "2015-05-26T18:04:27.763Z") + .field("state", ackState) + .endObject() + .startObject("last_execution") + .field("timestamp", "2015-05-25T18:04:27.733Z") + .field("successful", false) + .field("reason", "failed to send email") + .endObject() + .startObject("last_successful_execution") + .field("timestamp", "2015-05-25T18:04:27.773Z") + .field("successful", true) + .endObject() + .startObject("last_throttle") + .field("timestamp", "2015-04-25T18:05:23.445Z") + .field("reason", "throttling interval is set to [5 seconds] ...") + .endObject() + .endObject() + .endObject() + .endObject(); + } + + private WatchStatus parse(XContentType contentType, BytesReference bytes) throws IOException { + XContentParser parser = XContentFactory.xContent(contentType) + .createParser(NamedXContentRegistry.EMPTY, null, bytes.streamInput()); + parser.nextToken(); + + return WatchStatus.parse(parser); + } +}