diff --git a/pom.xml b/pom.xml index 2b45300f2a8..e4bdb41ee88 100644 --- a/pom.xml +++ b/pom.xml @@ -86,6 +86,20 @@ test + + org.mockito + mockito-core + 1.9.5 + test + + + + org.objenesis + objenesis + 2.1 + test + + com.google.guava diff --git a/src/main/java/org/elasticsearch/alerts/AlertsModule.java b/src/main/java/org/elasticsearch/alerts/AlertsModule.java index e7b27cfd9a4..721836b7488 100644 --- a/src/main/java/org/elasticsearch/alerts/AlertsModule.java +++ b/src/main/java/org/elasticsearch/alerts/AlertsModule.java @@ -13,6 +13,7 @@ import org.elasticsearch.alerts.rest.AlertsRestModule; import org.elasticsearch.alerts.scheduler.SchedulerModule; import org.elasticsearch.alerts.support.TemplateUtils; import org.elasticsearch.alerts.support.init.InitializingModule; +import org.elasticsearch.alerts.support.template.TemplateModule; import org.elasticsearch.alerts.transform.TransformModule; import org.elasticsearch.alerts.transport.AlertsTransportModule; import org.elasticsearch.alerts.condition.ConditionModule; @@ -28,6 +29,7 @@ public class AlertsModule extends AbstractModule implements SpawnModules { public Iterable spawnModules() { return ImmutableList.of( new InitializingModule(), + new TemplateModule(), new AlertsClientModule(), new TransformModule(), new AlertsRestModule(), diff --git a/src/main/java/org/elasticsearch/alerts/actions/Action.java b/src/main/java/org/elasticsearch/alerts/actions/Action.java index 5acff70ef89..63b83a22be7 100644 --- a/src/main/java/org/elasticsearch/alerts/actions/Action.java +++ b/src/main/java/org/elasticsearch/alerts/actions/Action.java @@ -8,6 +8,7 @@ package org.elasticsearch.alerts.actions; import org.elasticsearch.alerts.ExecutionContext; import org.elasticsearch.alerts.Payload; import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.collect.ImmutableMap; import org.elasticsearch.common.logging.ESLogger; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; @@ -19,8 +20,8 @@ import java.io.IOException; */ public abstract class Action implements ToXContent { - public static final String ALERT_NAME_VARIABLE_NAME = "alert_name"; - public static final String RESPONSE_VARIABLE_NAME = "response"; + public static final String ALERT_NAME_VARIABLE = "alert_name"; + public static final String PAYLOAD_VARIABLE = "payload"; protected final ESLogger logger; @@ -38,7 +39,12 @@ public abstract class Action implements ToXContent { */ public abstract R execute(ExecutionContext context, Payload payload) throws IOException; - + protected static ImmutableMap templateModel(ExecutionContext ctx, Payload payload) { + return ImmutableMap.builder() + .put(ALERT_NAME_VARIABLE, ctx.alert().name()) + .put(PAYLOAD_VARIABLE, payload.data()) + .build(); + } /** * Parses xcontent to a concrete action of the same type. */ @@ -57,8 +63,6 @@ public abstract class Action implements ToXContent { R parseResult(XContentParser parser) throws IOException; } - - public static abstract class Result implements ToXContent { public static final ParseField SUCCESS_FIELD = new ParseField("success"); diff --git a/src/main/java/org/elasticsearch/alerts/actions/ActionSettingsException.java b/src/main/java/org/elasticsearch/alerts/actions/ActionSettingsException.java new file mode 100644 index 00000000000..08dd19018d5 --- /dev/null +++ b/src/main/java/org/elasticsearch/alerts/actions/ActionSettingsException.java @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.alerts.actions; + +/** + * + */ +public class ActionSettingsException extends ActionException { + + public ActionSettingsException(String msg) { + super(msg); + } + + public ActionSettingsException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/src/main/java/org/elasticsearch/alerts/actions/email/EmailAction.java b/src/main/java/org/elasticsearch/alerts/actions/email/EmailAction.java index 049560b4568..b4794f59d33 100644 --- a/src/main/java/org/elasticsearch/alerts/actions/email/EmailAction.java +++ b/src/main/java/org/elasticsearch/alerts/actions/email/EmailAction.java @@ -8,9 +8,11 @@ package org.elasticsearch.alerts.actions.email; import org.elasticsearch.alerts.ExecutionContext; import org.elasticsearch.alerts.Payload; import org.elasticsearch.alerts.actions.Action; +import org.elasticsearch.alerts.actions.ActionSettingsException; import org.elasticsearch.alerts.actions.email.service.*; -import org.elasticsearch.alerts.support.StringTemplateUtils; +import org.elasticsearch.alerts.support.template.Template; import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.collect.ImmutableMap; import org.elasticsearch.common.component.AbstractComponent; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.logging.ESLogger; @@ -20,8 +22,6 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import java.io.IOException; -import java.util.HashMap; -import java.util.Map; /** */ @@ -33,21 +33,18 @@ public class EmailAction extends Action { private final Authentication auth; private final Profile profile; private final String account; - private final StringTemplateUtils.Template subject; - private final StringTemplateUtils.Template textBody; - private final StringTemplateUtils.Template htmlBody; + private final Template subject; + private final Template textBody; + private final Template htmlBody; private final boolean attachPayload; private final EmailService emailService; - private final StringTemplateUtils templateUtils; - public EmailAction(ESLogger logger, EmailService emailService, StringTemplateUtils templateUtils, - Email.Builder email, Authentication auth, Profile profile, String account, - StringTemplateUtils.Template subject, StringTemplateUtils.Template textBody, - StringTemplateUtils.Template htmlBody, boolean attachPayload) { + public EmailAction(ESLogger logger, EmailService emailService, Email.Builder email, Authentication auth, Profile profile, + String account, Template subject, Template textBody, Template htmlBody, boolean attachPayload) { + super(logger); this.emailService = emailService; - this.templateUtils = templateUtils; this.email = email; this.auth = auth; this.profile = profile; @@ -65,21 +62,14 @@ public class EmailAction extends Action { @Override public Result execute(ExecutionContext ctx, Payload payload) throws IOException { + ImmutableMap model = templateModel(ctx, payload); + email.id(ctx.id()); - - Map alertParams = new HashMap<>(); - alertParams.put(Action.ALERT_NAME_VARIABLE_NAME, ctx.alert().name()); - alertParams.put(RESPONSE_VARIABLE_NAME, payload.data()); - - String text = templateUtils.executeTemplate(subject, alertParams); - email.subject(text); - - text = templateUtils.executeTemplate(textBody, alertParams); - email.textBody(text); + email.subject(subject.render(model)); + email.textBody(textBody.render(model)); if (htmlBody != null) { - text = templateUtils.executeTemplate(htmlBody, alertParams); - email.htmlBody(text); + email.htmlBody(htmlBody.render(model)); } if (attachPayload) { @@ -100,17 +90,17 @@ public class EmailAction extends Action { public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); if (account != null) { - builder.field(Parser.ACCOUNT_FIELD.getPreferredName(), account); + builder.field(EmailAction.Parser.ACCOUNT_FIELD.getPreferredName(), account); } if (profile != null) { - builder.field(Parser.PROFILE_FIELD.getPreferredName(), profile); + builder.field(EmailAction.Parser.PROFILE_FIELD.getPreferredName(), profile); } builder.field(Email.TO_FIELD.getPreferredName(), (ToXContent) email.to()); if (subject != null) { - StringTemplateUtils.writeTemplate(Email.SUBJECT_FIELD.getPreferredName(), subject, builder, params); + builder.field(Email.SUBJECT_FIELD.getPreferredName(), subject); } if (textBody != null) { - StringTemplateUtils.writeTemplate(Email.TEXT_BODY_FIELD.getPreferredName(), textBody, builder, params); + builder.field(Email.TEXT_BODY_FIELD.getPreferredName(), textBody); } return builder.endObject(); } @@ -125,14 +115,14 @@ public class EmailAction extends Action { public static final ParseField EMAIL_FIELD = new ParseField("email"); public static final ParseField REASON_FIELD = new ParseField("reason"); - private final StringTemplateUtils templateUtils; + private final Template.Parser templateParser; private final EmailService emailService; @Inject - public Parser(Settings settings, EmailService emailService, StringTemplateUtils templateUtils) { + public Parser(Settings settings, EmailService emailService, Template.Parser templateParser) { super(settings); this.emailService = emailService; - this.templateUtils = templateUtils; + this.templateParser = templateParser; } @Override @@ -147,9 +137,9 @@ public class EmailAction extends Action { String account = null; Profile profile = null; Email.Builder email = Email.builder(); - StringTemplateUtils.Template subject = null; - StringTemplateUtils.Template textBody = null; - StringTemplateUtils.Template htmlBody = null; + Template subject = null; + Template textBody = null; + Template htmlBody = null; boolean attachPayload = false; String currentFieldName = null; @@ -169,15 +159,27 @@ public class EmailAction extends Action { } else if (Email.BCC_FIELD.match(currentFieldName)) { email.bcc(Email.AddressList.parse(currentFieldName, token, parser)); } else if (Email.SUBJECT_FIELD.match(currentFieldName)) { - subject = StringTemplateUtils.readTemplate(parser); + try { + subject = templateParser.parse(parser); + } catch (Template.Parser.ParseException pe) { + throw new ActionSettingsException("could not parse email [subject] template", pe); + } } else if (Email.TEXT_BODY_FIELD.match(currentFieldName)) { - textBody = StringTemplateUtils.readTemplate(parser); + try { + textBody = templateParser.parse(parser); + } catch (Template.Parser.ParseException pe) { + throw new ActionSettingsException("could not parse email [text_body] template", pe); + } + } else if (Email.HTML_BODY_FIELD.match(currentFieldName)) { + try { + htmlBody = templateParser.parse(parser); + } catch (Template.Parser.ParseException pe) { + throw new ActionSettingsException("could not parse email [html_body] template", pe); + } } else if (token == XContentParser.Token.VALUE_STRING) { if (Email.PRIORITY_FIELD.match(currentFieldName)) { email.priority(Email.Priority.resolve(parser.text())); - } else if (Email.HTML_BODY_FIELD.match(currentFieldName)) { - htmlBody = StringTemplateUtils.readTemplate(parser); - } else if (ACCOUNT_FIELD.match(currentFieldName)) { + } else if (ACCOUNT_FIELD.match(currentFieldName)) { account = parser.text(); } else if (USER_FIELD.match(currentFieldName)) { user = parser.text(); @@ -186,26 +188,26 @@ public class EmailAction extends Action { } else if (PROFILE_FIELD.match(currentFieldName)) { profile = Profile.resolve(parser.text()); } else { - throw new EmailException("could not parse email action. unrecognized string field [" + currentFieldName + "]"); + throw new ActionSettingsException("could not parse email action. unrecognized string field [" + currentFieldName + "]"); } } else if (token == XContentParser.Token.VALUE_BOOLEAN) { if (ATTACH_PAYLOAD_FIELD.match(currentFieldName)) { attachPayload = parser.booleanValue(); } else { - throw new EmailException("could not parse email action. unrecognized boolean field [" + currentFieldName + "]"); + throw new ActionSettingsException("could not parse email action. unrecognized boolean field [" + currentFieldName + "]"); } } else { - throw new EmailException("could not parse email action. unexpected token [" + token + "]"); + throw new ActionSettingsException("could not parse email action. unexpected token [" + token + "]"); } } } if (email.to() == null || email.to().isEmpty()) { - throw new EmailException("could not parse email action. [to] was not found or was empty"); + throw new ActionSettingsException("could not parse email action. [to] was not found or was empty"); } Authentication auth = new Authentication(user, password); - return new EmailAction(logger, emailService, templateUtils, email, auth, profile, account, subject, textBody, htmlBody, attachPayload); + return new EmailAction(logger, emailService, email, auth, profile, account, subject, textBody, htmlBody, attachPayload); } @Override diff --git a/src/main/java/org/elasticsearch/alerts/actions/index/IndexAction.java b/src/main/java/org/elasticsearch/alerts/actions/index/IndexAction.java index 95486756f90..c3821e919c1 100644 --- a/src/main/java/org/elasticsearch/alerts/actions/index/IndexAction.java +++ b/src/main/java/org/elasticsearch/alerts/actions/index/IndexAction.java @@ -12,6 +12,7 @@ import org.elasticsearch.alerts.ExecutionContext; import org.elasticsearch.alerts.Payload; import org.elasticsearch.alerts.actions.Action; import org.elasticsearch.alerts.actions.ActionException; +import org.elasticsearch.alerts.actions.ActionSettingsException; import org.elasticsearch.alerts.support.init.proxy.ClientProxy; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.component.AbstractComponent; @@ -90,6 +91,26 @@ public class IndexAction extends Action { return builder; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + IndexAction that = (IndexAction) o; + + if (index != null ? !index.equals(that.index) : that.index != null) return false; + if (type != null ? !type.equals(that.type) : that.type != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = index != null ? index.hashCode() : 0; + result = 31 * result + (type != null ? type.hashCode() : 0); + return result; + } + public static class Parser extends AbstractComponent implements Action.Parser { public static final ParseField INDEX_FIELD = new ParseField("index"); @@ -127,19 +148,19 @@ public class IndexAction extends Action { } else if (TYPE_FIELD.match(currentFieldName)) { type = parser.text(); } else { - throw new ActionException("could not parse index action. unexpected field [" + currentFieldName + "]"); + throw new ActionSettingsException("could not parse index action. unexpected field [" + currentFieldName + "]"); } } else { - throw new ActionException("could not parse index action. unexpected token [" + token + "]"); + throw new ActionSettingsException("could not parse index action. unexpected token [" + token + "]"); } } if (index == null) { - throw new ActionException("could not parse index action [index] is required"); + throw new ActionSettingsException("could not parse index action [index] is required"); } if (type == null) { - throw new ActionException("could not parse index action [type] is required"); + throw new ActionSettingsException("could not parse index action [type] is required"); } return new IndexAction(logger, client, index, type); @@ -215,24 +236,4 @@ public class IndexAction extends Action { } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - IndexAction that = (IndexAction) o; - - if (index != null ? !index.equals(that.index) : that.index != null) return false; - if (type != null ? !type.equals(that.type) : that.type != null) return false; - - return true; - } - - @Override - public int hashCode() { - int result = index != null ? index.hashCode() : 0; - result = 31 * result + (type != null ? type.hashCode() : 0); - return result; - } } diff --git a/src/main/java/org/elasticsearch/alerts/actions/webhook/WebhookAction.java b/src/main/java/org/elasticsearch/alerts/actions/webhook/WebhookAction.java index 1f72d18ac85..885a474b728 100644 --- a/src/main/java/org/elasticsearch/alerts/actions/webhook/WebhookAction.java +++ b/src/main/java/org/elasticsearch/alerts/actions/webhook/WebhookAction.java @@ -5,13 +5,17 @@ */ package org.elasticsearch.alerts.actions.webhook; +import org.elasticsearch.alerts.AlertsSettingsException; import org.elasticsearch.alerts.ExecutionContext; import org.elasticsearch.alerts.Payload; import org.elasticsearch.alerts.actions.Action; import org.elasticsearch.alerts.actions.ActionException; -import org.elasticsearch.alerts.support.StringTemplateUtils; +import org.elasticsearch.alerts.actions.ActionSettingsException; +import org.elasticsearch.alerts.support.template.Template; +import org.elasticsearch.alerts.support.template.XContentTemplate; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.collect.ImmutableMap; import org.elasticsearch.common.component.AbstractComponent; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.logging.ESLogger; @@ -21,8 +25,6 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import java.io.IOException; -import java.util.HashMap; -import java.util.Map; /** */ @@ -30,27 +32,18 @@ public class WebhookAction extends Action { public static final String TYPE = "webhook"; - static final StringTemplateUtils.Template DEFAULT_BODY_TEMPLATE = new StringTemplateUtils.Template( - "{ 'alertname' : '{{alert_name}}', 'response' : {{response}} }"); - - private final StringTemplateUtils templateUtils; private final HttpClient httpClient; - private final StringTemplateUtils.Template urlTemplate; private final HttpMethod method; + private final Template url; + private final @Nullable Template body; - //Optional, default will be used if not provided - private final StringTemplateUtils.Template bodyTemplate; - - public WebhookAction(ESLogger logger, StringTemplateUtils templateUtils, HttpClient httpClient, - @Nullable StringTemplateUtils.Template bodyTemplate, - StringTemplateUtils.Template urlTemplate, HttpMethod method) { + public WebhookAction(ESLogger logger, HttpClient httpClient, HttpMethod method, Template url, Template body) { super(logger); - this.templateUtils = templateUtils; this.httpClient = httpClient; - this.bodyTemplate = bodyTemplate; - this.urlTemplate = urlTemplate; this.method = method; + this.url = url; + this.body = body; } @Override @@ -60,44 +53,62 @@ public class WebhookAction extends Action { @Override public Result execute(ExecutionContext ctx, Payload payload) throws IOException { - Map data = payload.data(); - String renderedUrl = applyTemplate(templateUtils, urlTemplate, ctx.alert().name(), data); - String body = applyTemplate(templateUtils, bodyTemplate != null ? bodyTemplate : DEFAULT_BODY_TEMPLATE, ctx.alert().name(), data); + ImmutableMap model = ImmutableMap.builder() + .put(ALERT_NAME_VARIABLE, ctx.alert().name()) + .put(PAYLOAD_VARIABLE, payload.data()) + .build(); + String urlText = url.render(model); + String bodyText = body != null ? body.render(model) : XContentTemplate.YAML.render(model); try { - int status = httpClient.execute(method, renderedUrl, body); + int status = httpClient.execute(method, urlText, bodyText); if (status >= 400) { - logger.warn("got status [" + status + "] when connecting to [" + renderedUrl + "]"); + logger.warn("got status [" + status + "] when connecting to [" + urlText + "]"); } else { if (status >= 300) { logger.warn("a 200 range return code was expected, but got [" + status + "]"); } } - return new Result.Executed(status, renderedUrl, body); + return new Result.Executed(status, urlText, bodyText); + } catch (IOException ioe) { - logger.error("failed to connect to [{}] for alert [{}]", ioe, renderedUrl, ctx.alert().name()); + logger.error("failed to connect to [{}] for alert [{}]", ioe, urlText, ctx.alert().name()); return new Result.Failure("failed to send http request. " + ioe.getMessage()); } } - static String applyTemplate(StringTemplateUtils templateUtils, StringTemplateUtils.Template template, String alertName, Map data) { - Map webHookParams = new HashMap<>(); - webHookParams.put(ALERT_NAME_VARIABLE_NAME, alertName); - webHookParams.put(RESPONSE_VARIABLE_NAME, data); - return templateUtils.executeTemplate(template, webHookParams); - } - @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); builder.field(Parser.METHOD_FIELD.getPreferredName(), method.getName()); - if (bodyTemplate != null) { - StringTemplateUtils.writeTemplate(Parser.BODY_TEMPLATE_FIELD.getPreferredName(), bodyTemplate, builder, params); + builder.field(Parser.URL_FIELD.getPreferredName(), url); + if (body != null) { + builder.field(Parser.BODY_FIELD.getPreferredName(), body); } - StringTemplateUtils.writeTemplate(Parser.URL_TEMPLATE_FIELD.getPreferredName(), urlTemplate, builder, params); - builder.endObject(); - return builder; + return builder.endObject(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + WebhookAction that = (WebhookAction) o; + + if (!body.equals(that.body)) return false; + if (!method.equals(that.method)) return false; + if (!url.equals(that.url)) return false; + + return true; + } + + @Override + public int hashCode() { + int result = method.hashCode(); + result = 31 * result + url.hashCode(); + result = 31 * result + body.hashCode(); + return result; } public abstract static class Result extends Action.Result { @@ -108,19 +119,19 @@ public class WebhookAction extends Action { public static class Executed extends Result { - private final int httpStatusCode; + private final int httpStatus; private final String url; private final String body; - public Executed(int httpStatusCode, String url, String body) { - super(TYPE, httpStatusCode < 400); - this.httpStatusCode = httpStatusCode; + public Executed(int httpStatus, String url, String body) { + super(TYPE, httpStatus < 400); + this.httpStatus = httpStatus; this.url = url; this.body = body; } - public int httpStatusCode() { - return httpStatusCode; + public int httpStatus() { + return httpStatus; } public String url() { @@ -134,9 +145,9 @@ public class WebhookAction extends Action { @Override protected XContentBuilder xContentBody(XContentBuilder builder, Params params) throws IOException { return builder.field("success", success()) - .field(Parser.HTTP_STATUS_FIELD.getPreferredName(), httpStatusCode) - .field(Parser.URL_FIELD.getPreferredName(), url) - .field(Parser.BODY_FIELD.getPreferredName(), body); + .field(WebhookAction.Parser.HTTP_STATUS_FIELD.getPreferredName(), httpStatus) + .field(WebhookAction.Parser.URL_FIELD.getPreferredName(), url) + .field(WebhookAction.Parser.BODY_FIELD.getPreferredName(), body); } } @@ -151,7 +162,7 @@ public class WebhookAction extends Action { @Override protected XContentBuilder xContentBody(XContentBuilder builder, Params params) throws IOException { - return builder.field(Parser.REASON_FIELD.getPreferredName(), reason); + return builder.field(WebhookAction.Parser.REASON_FIELD.getPreferredName(), reason); } } } @@ -160,21 +171,18 @@ public class WebhookAction extends Action { public static class Parser extends AbstractComponent implements Action.Parser { public static final ParseField METHOD_FIELD = new ParseField("method"); - public static final ParseField URL_TEMPLATE_FIELD = new ParseField("url_template"); - public static final ParseField BODY_TEMPLATE_FIELD = new ParseField("body_template"); - - public static final ParseField BODY_FIELD = new ParseField("body"); public static final ParseField URL_FIELD = new ParseField("url"); + public static final ParseField BODY_FIELD = new ParseField("body"); public static final ParseField HTTP_STATUS_FIELD = new ParseField("http_status"); public static final ParseField REASON_FIELD = new ParseField("reason"); - private final StringTemplateUtils templateUtils; + private final Template.Parser templateParser; private final HttpClient httpClient; @Inject - public Parser(Settings settings, StringTemplateUtils templateUtils, HttpClient httpClient) { + public Parser(Settings settings, Template.Parser templateParser, HttpClient httpClient) { super(settings); - this.templateUtils = templateUtils; + this.templateParser = templateParser; this.httpClient = httpClient; } @@ -186,8 +194,8 @@ public class WebhookAction extends Action { @Override public WebhookAction parse(XContentParser parser) throws IOException { HttpMethod method = HttpMethod.POST; - StringTemplateUtils.Template urlTemplate = null; - StringTemplateUtils.Template bodyTemplate = null; + Template urlTemplate = null; + Template bodyTemplate = null; String currentFieldName = null; XContentParser.Token token; @@ -199,26 +207,33 @@ public class WebhookAction extends Action { if (METHOD_FIELD.match(currentFieldName)) { method = HttpMethod.valueOf(parser.text()); if (method != HttpMethod.POST && method != HttpMethod.GET && method != HttpMethod.PUT) { - throw new ActionException("could not parse webhook action. unsupported http method [" - + method.getName() + "]"); + throw new ActionSettingsException("could not parse webhook action. unsupported http method [" + method.getName() + "]"); + } + } else if (URL_FIELD.match(currentFieldName)) { + try { + urlTemplate = templateParser.parse(parser); + } catch (Template.Parser.ParseException pe) { + throw new AlertsSettingsException("could not parse webhook action [url] template", pe); + } + } else if (BODY_FIELD.match(currentFieldName)) { + try { + bodyTemplate = templateParser.parse(parser); + } catch (Template.Parser.ParseException pe) { + throw new ActionSettingsException("could not parse webhook action [body] template", pe); } - } else if (URL_TEMPLATE_FIELD.match(currentFieldName)) { - urlTemplate = StringTemplateUtils.readTemplate(parser); - } else if (BODY_TEMPLATE_FIELD.match(currentFieldName)) { - bodyTemplate = StringTemplateUtils.readTemplate(parser); } else { - throw new ActionException("could not parse webhook action. unexpected field [" + currentFieldName + "]"); + throw new ActionSettingsException("could not parse webhook action. unexpected field [" + currentFieldName + "]"); } } else { - throw new ActionException("could not parse webhook action. unexpected token [" + token + "]"); + throw new ActionSettingsException("could not parse webhook action. unexpected token [" + token + "]"); } } if (urlTemplate == null) { - throw new ActionException("could not parse webhook action. [url_template] is required"); + throw new ActionSettingsException("could not parse webhook action. [url_template] is required"); } - return new WebhookAction(logger, templateUtils, httpClient, bodyTemplate, urlTemplate, method); + return new WebhookAction(logger, httpClient, method, urlTemplate, bodyTemplate); } @Override @@ -264,25 +279,4 @@ public class WebhookAction extends Action { } } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - WebhookAction that = (WebhookAction) o; - - if (bodyTemplate != null ? !bodyTemplate.equals(that.bodyTemplate) : that.bodyTemplate != null) return false; - if (method != null ? !method.equals(that.method) : that.method != null) return false; - if (urlTemplate != null ? !urlTemplate.equals(that.urlTemplate) : that.urlTemplate != null) return false; - - return true; - } - - @Override - public int hashCode() { - int result = urlTemplate != null ? urlTemplate.hashCode() : 0; - result = 31 * result + (method != null ? method.hashCode() : 0); - result = 31 * result + (bodyTemplate != null ? bodyTemplate.hashCode() : 0); - return result; - } } diff --git a/src/main/java/org/elasticsearch/alerts/support/AlertUtils.java b/src/main/java/org/elasticsearch/alerts/support/AlertUtils.java index 5011cd87b10..39ae9e1e16b 100644 --- a/src/main/java/org/elasticsearch/alerts/support/AlertUtils.java +++ b/src/main/java/org/elasticsearch/alerts/support/AlertUtils.java @@ -134,7 +134,7 @@ public final class AlertUtils { searchRequest.templateName(parser.textOrNull()); break; case "template_type": - searchRequest.templateType(readScriptType(parser.textOrNull())); + searchRequest.templateType(ScriptService.ScriptType.valueOf(parser.text().toUpperCase(Locale.ROOT))); break; case "search_type": searchType = SearchType.fromString(parser.text()); @@ -169,7 +169,7 @@ public final class AlertUtils { builder.field("template_name", searchRequest.templateName()); } if (searchRequest.templateType() != null) { - builder.field("template_type", writeScriptType(searchRequest.templateType())); + builder.field("template_type", searchRequest.templateType().name().toLowerCase(Locale.ROOT)); } builder.startArray("indices"); for (String index : searchRequest.indices()) { @@ -200,30 +200,4 @@ public final class AlertUtils { builder.endObject(); } - static ScriptService.ScriptType readScriptType(String value) { - switch (value) { - case "indexed": - return ScriptService.ScriptType.INDEXED; - case "inline": - return ScriptService.ScriptType.INLINE; - case "file": - return ScriptService.ScriptType.FILE; - default: - throw new ElasticsearchIllegalArgumentException("Unknown script_type value [" + value + "]"); - } - } - - static String writeScriptType(ScriptService.ScriptType value) { - switch (value) { - case INDEXED: - return "indexed"; - case INLINE: - return "inline"; - case FILE: - return "file"; - default: - throw new ElasticsearchIllegalArgumentException("Illegal script_type value [" + value + "]"); - } - } - } diff --git a/src/main/java/org/elasticsearch/alerts/support/StringTemplateUtils.java b/src/main/java/org/elasticsearch/alerts/support/StringTemplateUtils.java deleted file mode 100644 index 6c9292cdcc1..00000000000 --- a/src/main/java/org/elasticsearch/alerts/support/StringTemplateUtils.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -package org.elasticsearch.alerts.support; - - -import org.elasticsearch.ElasticsearchIllegalArgumentException; -import org.elasticsearch.alerts.support.init.proxy.ScriptServiceProxy; -import org.elasticsearch.common.bytes.BytesReference; -import org.elasticsearch.common.component.AbstractComponent; -import org.elasticsearch.common.inject.Inject; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.xcontent.ToXContent; -import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.script.ExecutableScript; -import org.elasticsearch.script.ScriptService; - -import java.io.IOException; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -/** - */ -public class StringTemplateUtils extends AbstractComponent { - - private final ScriptServiceProxy scriptService; - - @Inject - public StringTemplateUtils(Settings settings, ScriptServiceProxy scriptService) { - super(settings); - this.scriptService = scriptService; - } - - public String executeTemplate(Template template) { - return executeTemplate(template, Collections.emptyMap()); - } - - public String executeTemplate(Template template, Map additionalParams) { - Map params = new HashMap<>(); - params.putAll(template.getParams()); - params.putAll(additionalParams); - ExecutableScript script = scriptService.executable(template.getLanguage(), template.getTemplate(), template.getScriptType(), params); - Object result = script.run(); - if (result instanceof String) { - return (String) result; - } else if (result instanceof BytesReference) { - return ((BytesReference) script.run()).toUtf8(); - } else { - return result.toString(); - } - } - public static Template readTemplate(XContentParser parser) throws IOException { - assert parser.currentToken() == XContentParser.Token.START_OBJECT : "Expected START_OBJECT, but was " + parser.currentToken(); - Map params = null; - String script = null; - ScriptService.ScriptType type = ScriptService.ScriptType.INLINE; - String language = "mustache"; - String fieldName = parser.currentName(); - for (XContentParser.Token token = parser.nextToken(); token != XContentParser.Token.END_OBJECT; token = parser.nextToken()) { - switch (token) { - case FIELD_NAME: - fieldName = parser.currentName(); - break; - case START_OBJECT: - switch (fieldName) { - case "params": - params = (Map) parser.map(); - break; - default: - throw new ElasticsearchIllegalArgumentException("Unexpected field [" + fieldName + "]"); - } - break; - case VALUE_STRING: - switch (fieldName) { - case "script": - script = parser.text(); - break; - case "language": - language = parser.text(); - break; - case "type": - type = AlertUtils.readScriptType(parser.text()); - break; - default: - throw new ElasticsearchIllegalArgumentException("Unexpected field [" + fieldName + "]"); - } - break; - default: - throw new ElasticsearchIllegalArgumentException("Unexpected json token [" + token + "]"); - } - } - return new Template(script, params, language, type); - } - - public static void writeTemplate(String objectName, Template template, XContentBuilder builder, ToXContent.Params params) throws IOException { - builder.startObject(objectName); - builder.field("script", template.getTemplate()); - builder.field("type", AlertUtils.writeScriptType(template.getScriptType())); - builder.field("language", template.getLanguage()); - if (template.getParams() != null && !template.getParams().isEmpty()) { - builder.field("params", template.getParams()); - } - builder.endObject(); - } - - public static class Template { - private final String template; - private final Map params; - private final String language; - private final ScriptService.ScriptType scriptType; - public Template(String template) { - this.template = template; - this.params = Collections.emptyMap(); - this.language = "mustache"; - this.scriptType = ScriptService.ScriptType.INLINE; - } - public Template(String template, Map params, String language, ScriptService.ScriptType scriptType) { - this.template = template; - this.params = params; - this.language = language; - this.scriptType = scriptType; - } - public ScriptService.ScriptType getScriptType() { - return scriptType; - } - public String getTemplate() { - return template; - } - public String getLanguage() { - return language; - } - public Map getParams() { - return params; - } - - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - Template template1 = (Template) o; - - if (language != null ? !language.equals(template1.language) : template1.language != null) return false; - if (params != null ? !params.equals(template1.params) : template1.params != null) return false; - if (scriptType != template1.scriptType) return false; - if (template != null ? !template.equals(template1.template) : template1.template != null) return false; - - return true; - } - - @Override - public int hashCode() { - int result = template != null ? template.hashCode() : 0; - result = 31 * result + (params != null ? params.hashCode() : 0); - result = 31 * result + (language != null ? language.hashCode() : 0); - result = 31 * result + (scriptType != null ? scriptType.hashCode() : 0); - return result; - } - } -} - diff --git a/src/main/java/org/elasticsearch/alerts/support/template/ScriptTemplate.java b/src/main/java/org/elasticsearch/alerts/support/template/ScriptTemplate.java new file mode 100644 index 00000000000..029bdc1a81f --- /dev/null +++ b/src/main/java/org/elasticsearch/alerts/support/template/ScriptTemplate.java @@ -0,0 +1,193 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.alerts.support.template; + +import org.elasticsearch.alerts.support.init.proxy.ScriptServiceProxy; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.base.Function; +import org.elasticsearch.common.base.Joiner; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.collect.Collections2; +import org.elasticsearch.common.collect.ImmutableList; +import org.elasticsearch.common.collect.ImmutableMap; +import org.elasticsearch.common.component.AbstractComponent; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.script.ExecutableScript; +import org.elasticsearch.script.ScriptService; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +/** + * + */ +public class ScriptTemplate implements ToXContent, Template { + + static final ParseField TEXT_FIELD = new ParseField("script", "text"); + static final ParseField LANG_FIELD = new ParseField("lang", "language", "script_lang"); + static final ParseField TYPE_FIELD = new ParseField("type", "script_type"); + static final ParseField PARAMS_FIELD = new ParseField("model", "params"); + + public static final String DEFAULT_LANG = "mustache"; + + private final String text; + private final String lang; + private final ScriptService.ScriptType type; + private final Map params; + private final ScriptServiceProxy service; + + public ScriptTemplate(ScriptServiceProxy service, String text) { + this(service, text, DEFAULT_LANG, ScriptService.ScriptType.INLINE, Collections.emptyMap()); + } + + public ScriptTemplate(ScriptServiceProxy service, String text, String lang, ScriptService.ScriptType type, Map params) { + this.service = service; + this.text = text; + this.lang = lang; + this.type = type; + this.params = params; + } + + public String text() { + return text; + } + + public ScriptService.ScriptType type() { + return type; + } + + public String lang() { + return lang; + } + + public Map params() { + return params; + } + + @Override + public String render(Map model) { + Map mergedModel = new HashMap<>(); + mergedModel.putAll(params); + mergedModel.putAll(model); + ExecutableScript script = service.executable(lang, text, type, mergedModel); + Object result = script.run(); + if (result instanceof BytesReference) { + return ((BytesReference) script.run()).toUtf8(); + } + return result.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ScriptTemplate template = (ScriptTemplate) o; + + if (!lang.equals(template.lang)) return false; + if (!params.equals(template.params)) return false; + if (!text.equals(template.text)) return false; + if (type != template.type) return false; + + return true; + } + + @Override + public int hashCode() { + int result = text.hashCode(); + result = 31 * result + lang.hashCode(); + result = 31 * result + type.hashCode(); + result = 31 * result + params.hashCode(); + return result; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject() + .field(TEXT_FIELD.getPreferredName(), text) + .field(TYPE_FIELD.getPreferredName(), type.name().toLowerCase(Locale.ROOT)) + .field(LANG_FIELD.getPreferredName(), lang) + .field(PARAMS_FIELD.getPreferredName(), this.params) + .endObject(); + } + + /** + */ + public static class Parser extends AbstractComponent implements Template.Parser { + + private final ScriptServiceProxy scriptService; + + @Inject + public Parser(Settings settings, ScriptServiceProxy scriptService) { + super(settings); + this.scriptService = scriptService; + } + + @Override + public ScriptTemplate parse(XContentParser parser) throws IOException { + assert parser.currentToken() == XContentParser.Token.START_OBJECT : "Expected START_OBJECT, but was " + parser.currentToken(); + + String text = null; + ScriptService.ScriptType type = ScriptService.ScriptType.INLINE; + String lang = DEFAULT_LANG; + ImmutableMap.Builder params = ImmutableMap.builder(); + + 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 (TEXT_FIELD.match(currentFieldName)) { + if (token.isValue()) { + text = parser.text(); + } else { + throw new ParseException("expected a string value for [" + currentFieldName + "], but found [" + token + "] instead"); + } + } else if (LANG_FIELD.match(currentFieldName)) { + if (token == XContentParser.Token.VALUE_STRING) { + lang = parser.text(); + } else { + throw new ParseException("expected a string value for [" + currentFieldName + "], but found [" + token + "] instead"); + } + } else if (TYPE_FIELD.match(currentFieldName)) { + if (token == XContentParser.Token.VALUE_STRING) { + String value = parser.text(); + try { + type = ScriptService.ScriptType.valueOf(value.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException iae) { + String typeOptions = Joiner.on(",").join(Collections2.transform(ImmutableList.copyOf(ScriptService.ScriptType.values()), new Function() { + @Override + public String apply(ScriptService.ScriptType scriptType) { + return scriptType.name().toLowerCase(Locale.ROOT); + } + })); + throw new ParseException("unknown template script type [" + currentFieldName + "]. type can only be on of: [" + typeOptions + "]"); + } + } + } else if (PARAMS_FIELD.match(currentFieldName)) { + if (token != XContentParser.Token.START_OBJECT) { + throw new ParseException("expected an object for [" + currentFieldName + "], but found [" + token + "]"); + } + params.putAll(parser.map()); + } else { + throw new ParseException("unexpected field [" + currentFieldName + "]"); + } + } + if (text == null) { + throw new ParseException("missing required field [" + TEXT_FIELD.getPreferredName() + "]"); + } + return new ScriptTemplate(scriptService, text, lang, type, params.build()); + } + } + +} diff --git a/src/main/java/org/elasticsearch/alerts/support/template/Template.java b/src/main/java/org/elasticsearch/alerts/support/template/Template.java new file mode 100644 index 00000000000..b60bd317988 --- /dev/null +++ b/src/main/java/org/elasticsearch/alerts/support/template/Template.java @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.alerts.support.template; + +import org.elasticsearch.alerts.AlertsException; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Map; + +/** + * + */ +public interface Template extends ToXContent { + + String render(Map model); + + interface Parser { + + T parse(XContentParser parser) throws IOException, ParseException; + + public static class ParseException extends AlertsException { + + public ParseException(String msg) { + super(msg); + } + } + + } +} diff --git a/src/main/java/org/elasticsearch/alerts/support/template/TemplateException.java b/src/main/java/org/elasticsearch/alerts/support/template/TemplateException.java new file mode 100644 index 00000000000..803ca634a6b --- /dev/null +++ b/src/main/java/org/elasticsearch/alerts/support/template/TemplateException.java @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.alerts.support.template; + +import org.elasticsearch.alerts.AlertsException; + +/** + * + */ +public class TemplateException extends AlertsException { + + public TemplateException(String msg) { + super(msg); + } + + public TemplateException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/src/main/java/org/elasticsearch/alerts/support/template/TemplateModule.java b/src/main/java/org/elasticsearch/alerts/support/template/TemplateModule.java new file mode 100644 index 00000000000..0e906f30127 --- /dev/null +++ b/src/main/java/org/elasticsearch/alerts/support/template/TemplateModule.java @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.alerts.support.template; + +import org.elasticsearch.common.inject.AbstractModule; + +/** + * + */ +public class TemplateModule extends AbstractModule { + + @Override + protected void configure() { + bind(Template.Parser.class).to(ScriptTemplate.Parser.class).asEagerSingleton(); + } +} diff --git a/src/main/java/org/elasticsearch/alerts/support/template/XContentTemplate.java b/src/main/java/org/elasticsearch/alerts/support/template/XContentTemplate.java new file mode 100644 index 00000000000..96474e20292 --- /dev/null +++ b/src/main/java/org/elasticsearch/alerts/support/template/XContentTemplate.java @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.alerts.support.template; + +import org.elasticsearch.common.xcontent.XContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.yaml.YamlXContent; + +import java.io.IOException; +import java.util.Map; + +/** + * + */ +public class XContentTemplate implements Template { + + public static XContentTemplate YAML = new XContentTemplate(YamlXContent.yamlXContent); + + private final XContent xContent; + + private XContentTemplate(XContent xContent) { + this.xContent = xContent; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject().endObject(); + } + + @Override + public String render(Map model) { + try { + return XContentBuilder.builder(xContent).map(model).bytes().toUtf8(); + } catch (IOException ioe) { + throw new TemplateException("could not render [" + xContent.type().name() + "] xcontent template", ioe); + } + } + +} diff --git a/src/test/java/org/elasticsearch/alerts/AbstractAlertingTests.java b/src/test/java/org/elasticsearch/alerts/AbstractAlertingTests.java index 6e65ef916e9..6315fed66ac 100644 --- a/src/test/java/org/elasticsearch/alerts/AbstractAlertingTests.java +++ b/src/test/java/org/elasticsearch/alerts/AbstractAlertingTests.java @@ -24,9 +24,10 @@ import org.elasticsearch.alerts.history.FiredAlert; import org.elasticsearch.alerts.history.HistoryStore; import org.elasticsearch.alerts.scheduler.schedule.CronSchedule; import org.elasticsearch.alerts.support.AlertUtils; -import org.elasticsearch.alerts.support.StringTemplateUtils; import org.elasticsearch.alerts.support.init.proxy.ClientProxy; import org.elasticsearch.alerts.support.init.proxy.ScriptServiceProxy; +import org.elasticsearch.alerts.support.template.ScriptTemplate; +import org.elasticsearch.alerts.support.template.Template; import org.elasticsearch.alerts.transform.SearchTransform; import org.elasticsearch.alerts.transport.actions.stats.AlertsStatsResponse; import org.elasticsearch.client.Client; @@ -162,10 +163,10 @@ public abstract class AbstractAlertingTests extends ElasticsearchIntegrationTest List actions = new ArrayList<>(); - StringTemplateUtils.Template template = - new StringTemplateUtils.Template("{{alert_name}} executed with {{response.hits.total}} hits"); + Template url = new ScriptTemplate(scriptService(), "http://localhost/foobarbaz/{{alert_name}}"); + Template body = new ScriptTemplate(scriptService(), "{{alert_name}} executed with {{response.hits.total}} hits"); - actions.add(new WebhookAction(logger, stringTemplateUtils(), httpClient(), template, new StringTemplateUtils.Template("http://localhost/foobarbaz/{{alert_name}}"), HttpMethod.GET)); + actions.add(new WebhookAction(logger, httpClient(), HttpMethod.GET, url, body)); Email.Address from = new Email.Address("from@test.com"); List emailAddressList = new ArrayList<>(); @@ -178,8 +179,8 @@ public abstract class AbstractAlertingTests extends ElasticsearchIntegrationTest emailBuilder.to(to); - EmailAction emailAction = new EmailAction(logger, noopEmailService(), stringTemplateUtils(), emailBuilder, - new Authentication("testname", "testpassword"), Profile.STANDARD, "testaccount", template, template, null, true); + EmailAction emailAction = new EmailAction(logger, noopEmailService(), emailBuilder, + new Authentication("testname", "testpassword"), Profile.STANDARD, "testaccount", body, body, null, true); actions.add(emailAction); @@ -189,9 +190,9 @@ public abstract class AbstractAlertingTests extends ElasticsearchIntegrationTest return new Alert( alertName, new CronSchedule("0/5 * * * * ? *"), - new ScriptSearchCondition(logger, ScriptServiceProxy.of(scriptService()), ClientProxy.of(client()), + new ScriptSearchCondition(logger, scriptService(), ClientProxy.of(client()), conditionRequest,"return true", ScriptService.ScriptType.INLINE, "groovy"), - new SearchTransform(logger, ScriptServiceProxy.of(scriptService()), ClientProxy.of(client()), transformRequest), + new SearchTransform(logger, scriptService(), ClientProxy.of(client()), transformRequest), new TimeValue(0), new Actions(actions), metadata, @@ -204,8 +205,12 @@ public abstract class AbstractAlertingTests extends ElasticsearchIntegrationTest return internalTestCluster().getInstance(AlertsClient.class); } - protected ScriptService scriptService() { - return internalTestCluster().getInstance(ScriptService.class); + protected ScriptServiceProxy scriptService() { + return internalTestCluster().getInstance(ScriptServiceProxy.class); + } + + protected Template.Parser templateParser() { + return internalTestCluster().getInstance(Template.Parser.class); } protected HttpClient httpClient() { @@ -216,10 +221,6 @@ public abstract class AbstractAlertingTests extends ElasticsearchIntegrationTest return new NoopEmailService(); } - protected StringTemplateUtils stringTemplateUtils() { - return internalTestCluster().getInstance(StringTemplateUtils.class); - } - protected FiredAlert.Parser firedAlertParser() { return internalTestCluster().getInstance(FiredAlert.Parser.class); } diff --git a/src/test/java/org/elasticsearch/alerts/AlertThrottleTests.java b/src/test/java/org/elasticsearch/alerts/AlertThrottleTests.java index ebdaf715668..d573baeea3c 100644 --- a/src/test/java/org/elasticsearch/alerts/AlertThrottleTests.java +++ b/src/test/java/org/elasticsearch/alerts/AlertThrottleTests.java @@ -15,16 +15,15 @@ import org.elasticsearch.alerts.actions.Action; import org.elasticsearch.alerts.actions.Actions; import org.elasticsearch.alerts.actions.index.IndexAction; import org.elasticsearch.alerts.client.AlertsClient; +import org.elasticsearch.alerts.condition.search.ScriptSearchCondition; import org.elasticsearch.alerts.history.FiredAlert; import org.elasticsearch.alerts.history.HistoryStore; import org.elasticsearch.alerts.scheduler.schedule.CronSchedule; import org.elasticsearch.alerts.support.init.proxy.ClientProxy; -import org.elasticsearch.alerts.support.init.proxy.ScriptServiceProxy; import org.elasticsearch.alerts.transform.SearchTransform; import org.elasticsearch.alerts.transport.actions.ack.AckAlertResponse; import org.elasticsearch.alerts.transport.actions.get.GetAlertResponse; import org.elasticsearch.alerts.transport.actions.put.PutAlertResponse; -import org.elasticsearch.alerts.condition.search.ScriptSearchCondition; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; @@ -40,9 +39,7 @@ import java.util.concurrent.TimeUnit; import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; import static org.elasticsearch.search.builder.SearchSourceBuilder.searchSource; -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.greaterThanOrEqualTo; -import static org.hamcrest.Matchers.lessThanOrEqualTo; +import static org.hamcrest.Matchers.*; import static org.hamcrest.core.IsEqual.equalTo; /** @@ -69,9 +66,9 @@ public class AlertThrottleTests extends AbstractAlertingTests { Alert alert = new Alert( "test-serialization", new CronSchedule("0/5 * * * * ? *"), - new ScriptSearchCondition(logger, ScriptServiceProxy.of(scriptService()), ClientProxy.of(client()), + new ScriptSearchCondition(logger, scriptService(), ClientProxy.of(client()), request, "hits.total > 0", ScriptService.ScriptType.INLINE, "groovy"), - new SearchTransform(logger, ScriptServiceProxy.of(scriptService()), ClientProxy.of(client()), request), + new SearchTransform(logger, scriptService(), ClientProxy.of(client()), request), new TimeValue(0), new Actions(actions), null, @@ -152,9 +149,9 @@ public class AlertThrottleTests extends AbstractAlertingTests { Alert alert = new Alert( "test-time-throttle", new CronSchedule("0/5 * * * * ? *"), - new ScriptSearchCondition(logger, ScriptServiceProxy.of(scriptService()), ClientProxy.of(client()), + new ScriptSearchCondition(logger, scriptService(), ClientProxy.of(client()), request, "hits.total > 0", ScriptService.ScriptType.INLINE, "groovy"), - new SearchTransform(logger, ScriptServiceProxy.of(scriptService()), ClientProxy.of(client()), request), + new SearchTransform(logger, scriptService(), ClientProxy.of(client()), request), new TimeValue(10, TimeUnit.SECONDS), new Actions(actions), null, diff --git a/src/test/java/org/elasticsearch/alerts/BootStrapTest.java b/src/test/java/org/elasticsearch/alerts/BootStrapTest.java index da2dc5c802f..82038a6321c 100644 --- a/src/test/java/org/elasticsearch/alerts/BootStrapTest.java +++ b/src/test/java/org/elasticsearch/alerts/BootStrapTest.java @@ -16,7 +16,6 @@ import org.elasticsearch.alerts.history.FiredAlert; import org.elasticsearch.alerts.history.HistoryStore; import org.elasticsearch.alerts.scheduler.schedule.CronSchedule; import org.elasticsearch.alerts.support.init.proxy.ClientProxy; -import org.elasticsearch.alerts.support.init.proxy.ScriptServiceProxy; import org.elasticsearch.alerts.transform.SearchTransform; import org.elasticsearch.alerts.transport.actions.put.PutAlertResponse; import org.elasticsearch.alerts.transport.actions.stats.AlertsStatsResponse; @@ -80,9 +79,9 @@ public class BootStrapTest extends AbstractAlertingTests { Alert alert = new Alert( "test-serialization", new CronSchedule("0/5 * * * * ? 2035"), - new ScriptSearchCondition(logger, ScriptServiceProxy.of(scriptService()), ClientProxy.of(client()), + new ScriptSearchCondition(logger, scriptService(), ClientProxy.of(client()), searchRequest, "return true", ScriptService.ScriptType.INLINE, "groovy"), - new SearchTransform(logger, ScriptServiceProxy.of(scriptService()), ClientProxy.of(client()), searchRequest), + new SearchTransform(logger, scriptService(), ClientProxy.of(client()), searchRequest), new TimeValue(0), new Actions(new ArrayList()), null, @@ -140,9 +139,9 @@ public class BootStrapTest extends AbstractAlertingTests { Alert alert = new Alert( "action-test-"+ i + " " + j, new CronSchedule("0/5 * * * * ? 2035"), //Set a cron schedule far into the future so this alert is never scheduled - new ScriptSearchCondition(logger, ScriptServiceProxy.of(scriptService()), ClientProxy.of(client()), + new ScriptSearchCondition(logger, scriptService(), ClientProxy.of(client()), searchRequest, "return true", ScriptService.ScriptType.INLINE, "groovy"), - new SearchTransform(logger, ScriptServiceProxy.of(scriptService()), ClientProxy.of(client()), searchRequest), + new SearchTransform(logger, scriptService(), ClientProxy.of(client()), searchRequest), new TimeValue(0), new Actions(new ArrayList()), null, diff --git a/src/test/java/org/elasticsearch/alerts/TransformSearchTest.java b/src/test/java/org/elasticsearch/alerts/TransformSearchTest.java index 1dbb7ef9ea5..364082e712d 100644 --- a/src/test/java/org/elasticsearch/alerts/TransformSearchTest.java +++ b/src/test/java/org/elasticsearch/alerts/TransformSearchTest.java @@ -13,7 +13,6 @@ import org.elasticsearch.alerts.actions.index.IndexAction; import org.elasticsearch.alerts.condition.search.ScriptSearchCondition; import org.elasticsearch.alerts.scheduler.schedule.CronSchedule; import org.elasticsearch.alerts.support.init.proxy.ClientProxy; -import org.elasticsearch.alerts.support.init.proxy.ScriptServiceProxy; import org.elasticsearch.alerts.transform.SearchTransform; import org.elasticsearch.alerts.transport.actions.put.PutAlertResponse; import org.elasticsearch.common.unit.TimeValue; @@ -59,9 +58,9 @@ public class TransformSearchTest extends AbstractAlertingTests { Alert alert = new Alert( "test-serialization", new CronSchedule("0/5 * * * * ? *"), - new ScriptSearchCondition(logger, ScriptServiceProxy.of(scriptService()), ClientProxy.of(client()), + new ScriptSearchCondition(logger, scriptService(), ClientProxy.of(client()), conditionRequest,"return true", ScriptService.ScriptType.INLINE, "groovy"), - new SearchTransform(logger, ScriptServiceProxy.of(scriptService()), ClientProxy.of(client()), transformRequest), + new SearchTransform(logger, scriptService(), ClientProxy.of(client()), transformRequest), new TimeValue(0), new Actions(actions), metadata, diff --git a/src/test/java/org/elasticsearch/alerts/actions/ActionsTest.java b/src/test/java/org/elasticsearch/alerts/actions/ActionsTest.java index 283ae53655f..5446e4b8a32 100644 --- a/src/test/java/org/elasticsearch/alerts/actions/ActionsTest.java +++ b/src/test/java/org/elasticsearch/alerts/actions/ActionsTest.java @@ -9,9 +9,10 @@ package org.elasticsearch.alerts.actions; import org.elasticsearch.alerts.AbstractAlertingTests; import org.elasticsearch.alerts.Alert; import org.elasticsearch.alerts.actions.index.IndexAction; +import org.elasticsearch.alerts.condition.Condition; +import org.elasticsearch.alerts.condition.search.ScriptSearchCondition; import org.elasticsearch.alerts.scheduler.schedule.CronSchedule; import org.elasticsearch.alerts.support.init.proxy.ClientProxy; -import org.elasticsearch.alerts.support.init.proxy.ScriptServiceProxy; import org.elasticsearch.alerts.transform.SearchTransform; import org.elasticsearch.alerts.transport.actions.delete.DeleteAlertRequest; import org.elasticsearch.alerts.transport.actions.delete.DeleteAlertResponse; @@ -19,8 +20,6 @@ import org.elasticsearch.alerts.transport.actions.get.GetAlertRequest; import org.elasticsearch.alerts.transport.actions.get.GetAlertResponse; import org.elasticsearch.alerts.transport.actions.put.PutAlertRequest; import org.elasticsearch.alerts.transport.actions.put.PutAlertResponse; -import org.elasticsearch.alerts.condition.Condition; -import org.elasticsearch.alerts.condition.search.ScriptSearchCondition; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; @@ -57,7 +56,7 @@ public class ActionsTest extends AbstractAlertingTests { final List actionList = new ArrayList<>(); actionList.add(alertAction); - Condition alertCondition = new ScriptSearchCondition(logger, ScriptServiceProxy.of(scriptService()), + Condition alertCondition = new ScriptSearchCondition(logger, scriptService(), ClientProxy.of(client()), createConditionSearchRequest(), "return true", ScriptService.ScriptType.INLINE, "groovy"); @@ -65,7 +64,7 @@ public class ActionsTest extends AbstractAlertingTests { "my-first-alert", new CronSchedule("0/5 * * * * ? *"), alertCondition, - new SearchTransform(logger, ScriptServiceProxy.of(scriptService()), ClientProxy.of(client()), createConditionSearchRequest()), + new SearchTransform(logger, scriptService(), ClientProxy.of(client()), createConditionSearchRequest()), new TimeValue(0), new Actions(actionList), null, diff --git a/src/test/java/org/elasticsearch/alerts/actions/email/EmailActionTest.java b/src/test/java/org/elasticsearch/alerts/actions/email/EmailActionTest.java index 3bd28662398..38d29972222 100644 --- a/src/test/java/org/elasticsearch/alerts/actions/email/EmailActionTest.java +++ b/src/test/java/org/elasticsearch/alerts/actions/email/EmailActionTest.java @@ -13,8 +13,8 @@ import org.elasticsearch.alerts.actions.email.service.Email; import org.elasticsearch.alerts.actions.email.service.EmailService; import org.elasticsearch.alerts.actions.email.service.Profile; import org.elasticsearch.alerts.scheduler.schedule.CronSchedule; -import org.elasticsearch.alerts.support.StringTemplateUtils; import org.elasticsearch.alerts.support.init.proxy.ScriptServiceProxy; +import org.elasticsearch.alerts.support.template.ScriptTemplate; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.common.joda.time.DateTime; import org.elasticsearch.common.settings.ImmutableSettings; @@ -40,8 +40,6 @@ import java.util.Set; public class EmailActionTest extends ElasticsearchTestCase { public void testEmailTemplateRender() throws IOException, MessagingException { - StringTemplateUtils.Template template = - new StringTemplateUtils.Template("{{alert_name}} executed with {{response.hits.total}} hits"); Settings settings = ImmutableSettings.settingsBuilder().build(); MustacheScriptEngineService mustacheScriptEngineService = new MustacheScriptEngineService(settings); @@ -49,8 +47,9 @@ public class EmailActionTest extends ElasticsearchTestCase { Set engineServiceSet = new HashSet<>(); engineServiceSet.add(mustacheScriptEngineService); - ScriptService scriptService = new ScriptService(settings, new Environment(), engineServiceSet, new ResourceWatcherService(settings, threadPool)); - StringTemplateUtils stringTemplateUtils = new StringTemplateUtils(settings, ScriptServiceProxy.of(scriptService)); + ScriptServiceProxy scriptService = ScriptServiceProxy.of(new ScriptService(settings, new Environment(), engineServiceSet, new ResourceWatcherService(settings, threadPool))); + + ScriptTemplate template = new ScriptTemplate(scriptService, "{{alert_name}} executed with {{response.hits.total}} hits"); EmailService emailService = new EmailServiceMock(); @@ -64,7 +63,7 @@ public class EmailActionTest extends ElasticsearchTestCase { emailBuilder.from(from); emailBuilder.to(to); - EmailAction emailAction = new EmailAction(logger, emailService, stringTemplateUtils, emailBuilder, + EmailAction emailAction = new EmailAction(logger, emailService, emailBuilder, new Authentication("testname", "testpassword"), Profile.STANDARD, "testaccount", template, template, null, true); //This is ok since the execution of the action only relies on the alert name diff --git a/src/test/java/org/elasticsearch/alerts/actions/webhook/WebhookTest.java b/src/test/java/org/elasticsearch/alerts/actions/webhook/WebhookTest.java deleted file mode 100644 index 82c50763f8e..00000000000 --- a/src/test/java/org/elasticsearch/alerts/actions/webhook/WebhookTest.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -package org.elasticsearch.alerts.actions.webhook; - -import org.elasticsearch.alerts.support.StringTemplateUtils; -import org.elasticsearch.alerts.support.init.proxy.ScriptServiceProxy; -import org.elasticsearch.common.settings.ImmutableSettings; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.env.Environment; -import org.elasticsearch.script.ScriptEngineService; -import org.elasticsearch.script.ScriptService; -import org.elasticsearch.script.mustache.MustacheScriptEngineService; -import org.elasticsearch.test.ElasticsearchTestCase; -import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.watcher.ResourceWatcherService; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -/** - */ -public class WebhookTest extends ElasticsearchTestCase { - - public void testRequestParameterSerialization() throws Exception { - - Map responseMap = new HashMap<>(); - responseMap.put("hits",0); - - Settings settings = ImmutableSettings.settingsBuilder().build(); - MustacheScriptEngineService mustacheScriptEngineService = new MustacheScriptEngineService(settings); - ThreadPool tp; - tp = new ThreadPool(ThreadPool.Names.SAME); - Set engineServiceSet = new HashSet<>(); - engineServiceSet.add(mustacheScriptEngineService); - ScriptService scriptService = new ScriptService(settings, new Environment(), engineServiceSet, new ResourceWatcherService(settings, tp)); - - StringTemplateUtils.Template testTemplate = new StringTemplateUtils.Template("{ 'alertname' : '{{alert_name}}', 'response' : { 'hits' : {{response.hits}} } }"); - String testBody = WebhookAction.applyTemplate(new StringTemplateUtils(settings, ScriptServiceProxy.of(scriptService)), testTemplate, "foobar", responseMap); - - tp.shutdownNow(); - assertEquals("{ 'alertname' : 'foobar', 'response' : { 'hits' : 0 } }", testBody); - - - } -} diff --git a/src/test/java/org/elasticsearch/alerts/support/template/ScriptTemplateTests.java b/src/test/java/org/elasticsearch/alerts/support/template/ScriptTemplateTests.java new file mode 100644 index 00000000000..5880eee9546 --- /dev/null +++ b/src/test/java/org/elasticsearch/alerts/support/template/ScriptTemplateTests.java @@ -0,0 +1,171 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.alerts.support.template; + +import org.elasticsearch.alerts.support.init.proxy.ScriptServiceProxy; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.collect.ImmutableMap; +import org.elasticsearch.common.settings.ImmutableSettings; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.script.ExecutableScript; +import org.elasticsearch.script.ScriptService; +import org.elasticsearch.test.ElasticsearchTestCase; +import org.junit.Before; +import org.junit.Test; + +import java.util.Map; + +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.hamcrest.Matchers.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * + */ +public class ScriptTemplateTests extends ElasticsearchTestCase { + + private ScriptServiceProxy proxy; + private ExecutableScript script; + + @Before + public void init() throws Exception { + proxy = mock(ScriptServiceProxy.class); + script = mock(ExecutableScript.class); + } + + @Test + public void testRender() throws Exception { + String lang = "_lang"; + String templateText = "_template"; + Map params = ImmutableMap.of("param_key", "param_val"); + Map model = ImmutableMap.of("model_key", "model_val"); + Map merged = ImmutableMap.builder().putAll(params).putAll(model).build(); + ScriptService.ScriptType scriptType = ScriptService.ScriptType.values()[randomIntBetween(0, ScriptService.ScriptType.values().length - 1)]; + + when(script.run()).thenReturn("rendered_text"); + when(proxy.executable(lang, templateText, scriptType, merged)).thenReturn(script); + + ScriptTemplate template = new ScriptTemplate(proxy, templateText, lang, scriptType, params); + assertThat(template.render(model), is("rendered_text")); + } + + @Test + public void testRender_OverridingModel() throws Exception { + String lang = "_lang"; + String templateText = "_template"; + Map params = ImmutableMap.of("key", "param_val"); + Map model = ImmutableMap.of("key", "model_val"); + ScriptService.ScriptType scriptType = randomScriptType(); + + + when(script.run()).thenReturn("rendered_text"); + when(proxy.executable(lang, templateText, scriptType, model)).thenReturn(script); + + ScriptTemplate template = new ScriptTemplate(proxy, templateText, lang, scriptType, params); + assertThat(template.render(model), is("rendered_text")); + } + + @Test + public void testRender_Defaults() throws Exception { + String templateText = "_template"; + Map model = ImmutableMap.of("key", "model_val"); + + when(script.run()).thenReturn("rendered_text"); + when(proxy.executable(ScriptTemplate.DEFAULT_LANG, templateText, ScriptService.ScriptType.INLINE, model)).thenReturn(script); + + ScriptTemplate template = new ScriptTemplate(proxy, templateText); + assertThat(template.render(model), is("rendered_text")); + } + + @Test + public void testParser() throws Exception { + ScriptTemplate.Parser templateParser = new ScriptTemplate.Parser(ImmutableSettings.EMPTY, proxy); + + ScriptTemplate template = new ScriptTemplate(proxy, "_template", "_lang", randomScriptType(), ImmutableMap.of("param_key", "param_val")); + + XContentBuilder builder = jsonBuilder().startObject() + .field(randomFrom("lang", "script_lang"), template.lang()) + .field(randomFrom("script", "text"), template.text()) + .field(randomFrom("type", "script_type"), template.type().name()) + .field(randomFrom("params", "model"), template.params()) + .endObject(); + BytesReference bytes = builder.bytes(); + XContentParser parser = JsonXContent.jsonXContent.createParser(bytes); + parser.nextToken(); + ScriptTemplate parsed = templateParser.parse(parser); + assertThat(parsed, notNullValue()); + assertThat(parsed, equalTo(template)); + } + + @Test + public void testParser_ParserSelfGenerated() throws Exception { + ScriptTemplate.Parser templateParser = new ScriptTemplate.Parser(ImmutableSettings.EMPTY, proxy); + + ScriptTemplate template = new ScriptTemplate(proxy, "_template", "_lang", randomScriptType(), ImmutableMap.of("param_key", "param_val")); + + XContentBuilder builder = jsonBuilder().value(template); + BytesReference bytes = builder.bytes(); + XContentParser parser = JsonXContent.jsonXContent.createParser(bytes); + parser.nextToken(); + ScriptTemplate parsed = templateParser.parse(parser); + assertThat(parsed, notNullValue()); + assertThat(parsed, equalTo(template)); + } + + @Test(expected = Template.Parser.ParseException.class) + public void testParser_Invalid_UnexpectedField() throws Exception { + ScriptTemplate.Parser templateParser = new ScriptTemplate.Parser(ImmutableSettings.EMPTY, proxy); + + XContentBuilder builder = jsonBuilder().startObject() + .field("unknown_field", "value") + .endObject(); + BytesReference bytes = builder.bytes(); + XContentParser parser = JsonXContent.jsonXContent.createParser(bytes); + parser.nextToken(); + templateParser.parse(parser); + fail("expected parse exception when encountering an unknown field"); + } + + @Test(expected = Template.Parser.ParseException.class) + public void testParser_Invalid_UnknownScriptType() throws Exception { + ScriptTemplate.Parser templateParser = new ScriptTemplate.Parser(ImmutableSettings.EMPTY, proxy); + + XContentBuilder builder = jsonBuilder().startObject() + .field("lang", ScriptTemplate.DEFAULT_LANG) + .field("script", "_template") + .field("type", "unknown_type") + .startObject("params").endObject() + .endObject(); + BytesReference bytes = builder.bytes(); + XContentParser parser = JsonXContent.jsonXContent.createParser(bytes); + parser.nextToken(); + templateParser.parse(parser); + fail("expected parse exception when script type is unknown"); + } + + @Test(expected = Template.Parser.ParseException.class) + public void testParser_Invalid_MissingScript() throws Exception { + ScriptTemplate.Parser templateParser = new ScriptTemplate.Parser(ImmutableSettings.EMPTY, proxy); + + XContentBuilder builder = jsonBuilder().startObject() + .field("lang", ScriptTemplate.DEFAULT_LANG) + .field("type", ScriptService.ScriptType.INDEXED) + .startObject("params").endObject() + .endObject(); + BytesReference bytes = builder.bytes(); + XContentParser parser = JsonXContent.jsonXContent.createParser(bytes); + parser.nextToken(); + templateParser.parse(parser); + fail("expected parse exception when template text is missing"); + } + + private static ScriptService.ScriptType randomScriptType() { + return randomFrom(ScriptService.ScriptType.values()); + } +} diff --git a/src/test/java/org/elasticsearch/alerts/support/template/XContentTemplateTests.java b/src/test/java/org/elasticsearch/alerts/support/template/XContentTemplateTests.java new file mode 100644 index 00000000000..5972197eda2 --- /dev/null +++ b/src/test/java/org/elasticsearch/alerts/support/template/XContentTemplateTests.java @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.alerts.support.template; + +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.yaml.YamlXContent; +import org.elasticsearch.test.ElasticsearchTestCase; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +/** + * + */ +public class XContentTemplateTests extends ElasticsearchTestCase { + + @Test + public void testYaml() throws Exception { + Map model = new HashMap<>(); + Map a = new HashMap<>(); + a.put("aa", "aa"); + a.put("ab", "ab"); + model.put("a", a); + Map b = new HashMap<>(); + b.put("ba", 21); + model.put("b", b); + model.put("c", "c"); + + String text = XContentTemplate.YAML.render(model); + + // now lets read it as xcontent and make sure it has all the pieces + XContentParser parser = YamlXContent.yamlXContent.createParser(new BytesArray(text)); + XContentParser.Token token = parser.nextToken(); + assertThat(token, is(XContentParser.Token.START_OBJECT)); + String currentFieldName = null; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if ("a".equals(currentFieldName)) { + assertThat(token, is(XContentParser.Token.START_OBJECT)); + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else { + assertThat(token, is(XContentParser.Token.VALUE_STRING)); + if ("aa".equals(currentFieldName)) { + assertThat(parser.text(), equalTo("aa")); + } else if ("ab".equals(currentFieldName)) { + assertThat(parser.text(), equalTo("ab")); + } else { + fail("unexpected field name [" + currentFieldName + "]"); + } + } + } + } else if ("b".equals(currentFieldName)) { + assertThat(token, is(XContentParser.Token.START_OBJECT)); + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else { + assertThat(token, is(XContentParser.Token.VALUE_NUMBER)); + assertThat(currentFieldName, equalTo("ba")); + assertThat(parser.intValue(), is(21)); + } + } + } else if ("c".equals(currentFieldName)) { + assertThat(token, is(XContentParser.Token.VALUE_STRING)); + assertThat(parser.text(), equalTo("c")); + } else { + fail("unexpected field [" + currentFieldName + "]"); + } + } + } +}