diff --git a/src/main/java/org/elasticsearch/alerts/support/Script.java b/src/main/java/org/elasticsearch/alerts/support/Script.java new file mode 100644 index 00000000000..91c4b10a47f --- /dev/null +++ b/src/main/java/org/elasticsearch/alerts/support/Script.java @@ -0,0 +1,161 @@ +/* + * 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.alerts.AlertsException; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.script.ScriptService; + +import java.io.IOException; +import java.util.Collections; +import java.util.Locale; +import java.util.Map; + +/** + * + */ +public class Script implements ToXContent { + + public static final ParseField SCRIPT_FIELD = new ParseField("script"); + public static final ParseField TYPE_FIELD = new ParseField("type"); + public static final ParseField LANG_FIELD = new ParseField("lang"); + public static final ParseField PARAMS_FIELD = new ParseField("params"); + + private final String script; + private final ScriptService.ScriptType type; + private final String lang; + private final Map params; + + public Script(String script) { + this(script, ScriptService.ScriptType.INLINE, ScriptService.DEFAULT_LANG, Collections.emptyMap()); + } + + public Script(String script, ScriptService.ScriptType type, String lang, Map params) { + this.script = script; + this.type = type; + this.lang = lang; + this.params = params; + } + + public String script() { + return script; + } + + public ScriptService.ScriptType type() { + return type; + } + + public String lang() { + return lang; + } + + public Map params() { + return params; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Script script1 = (Script) o; + + if (!lang.equals(script1.lang)) return false; + if (!params.equals(script1.params)) return false; + if (!script.equals(script1.script)) return false; + if (type != script1.type) return false; + + return true; + } + + @Override + public int hashCode() { + int result = script.hashCode(); + result = 31 * result + type.hashCode(); + result = 31 * result + lang.hashCode(); + result = 31 * result + params.hashCode(); + return result; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject() + .field(SCRIPT_FIELD.getPreferredName(), script) + .field(TYPE_FIELD.getPreferredName(), script) + .field(LANG_FIELD.getPreferredName(), lang) + .field(PARAMS_FIELD.getPreferredName(), params) + .endObject(); + } + + public static Script parse(XContentParser parser) throws IOException { + XContentParser.Token token = parser.currentToken(); + if (token == XContentParser.Token.VALUE_STRING) { + return new Script(parser.text()); + } + if (token != XContentParser.Token.START_OBJECT) { + throw new ParseException("expected a string value or an object, but found [" + token + "] instead"); + } + + String script = null; + ScriptService.ScriptType type = ScriptService.ScriptType.INLINE; + String lang = ScriptService.DEFAULT_LANG; + Map params = Collections.emptyMap(); + + String currentFieldName = null; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (SCRIPT_FIELD.match(currentFieldName)) { + if (token == XContentParser.Token.VALUE_STRING) { + script = parser.text(); + } else { + throw new ParseException("expected a string value for field [" + currentFieldName + "], but found [" + token + "]"); + } + } 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) { + throw new ParseException("unknown script type [" + value + "]"); + } + } + } else if (LANG_FIELD.match(currentFieldName)) { + if (token == XContentParser.Token.VALUE_STRING) { + lang = parser.text(); + } else { + throw new ParseException("expected a string value for field [" + currentFieldName + "], but found [" + token + "]"); + } + } else if (PARAMS_FIELD.match(currentFieldName)) { + if (token == XContentParser.Token.START_OBJECT) { + params = parser.map(); + } else { + throw new ParseException("expected an object for field [" + currentFieldName + "], but found [" + token + "]"); + } + } else { + throw new ParseException("unexpected field [" + currentFieldName + "]"); + } + } + if (script == null) { + throw new ParseException("missing required string field [" + currentFieldName + "]"); + } + return new Script(script, type, lang, params); + } + + public static class ParseException extends AlertsException { + + public ParseException(String msg) { + super(msg); + } + + public ParseException(String msg, Throwable cause) { + super(msg, cause); + } + } +} diff --git a/src/main/java/org/elasticsearch/alerts/transform/ScriptTransform.java b/src/main/java/org/elasticsearch/alerts/transform/ScriptTransform.java new file mode 100644 index 00000000000..59d1d10547d --- /dev/null +++ b/src/main/java/org/elasticsearch/alerts/transform/ScriptTransform.java @@ -0,0 +1,89 @@ +/* + * 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.transform; + +import org.elasticsearch.alerts.AlertsSettingsException; +import org.elasticsearch.alerts.ExecutionContext; +import org.elasticsearch.alerts.Payload; +import org.elasticsearch.alerts.support.Script; +import org.elasticsearch.alerts.support.init.proxy.ScriptServiceProxy; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.script.ExecutableScript; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * + */ +public class ScriptTransform extends Transform { + + public static final String TYPE = "script"; + + private final Script script; + private final ScriptServiceProxy scriptService; + + public ScriptTransform(Script script, ScriptServiceProxy scriptService) { + this.script = script; + this.scriptService = scriptService; + } + + @Override + public String type() { + return TYPE; + } + + Script script() { + return script; + } + + @Override + public Result apply(ExecutionContext ctx, Payload payload) throws IOException { + Map model = new HashMap<>(); + model.putAll(script.params()); + model.putAll(createModel(ctx, payload)); + ExecutableScript executable = scriptService.executable(script.lang(), script.script(), script.type(), model); + Object value = executable.run(); + if (!(value instanceof Map)) { + throw new TransformException("illegal [script] transform [" + script.script() + "]. script must output a Map structure but outputted [" + value.getClass().getSimpleName() + "] instead"); + } + return new Result(TYPE, new Payload.Simple((Map) value)); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.value(script); + } + + public static class Parser implements Transform.Parser { + + private final ScriptServiceProxy scriptService; + + @Inject + public Parser(ScriptServiceProxy scriptService) { + this.scriptService = scriptService; + } + + @Override + public String type() { + return TYPE; + } + + @Override + public ScriptTransform parse(XContentParser parser) throws IOException { + Script script = null; + try { + script = Script.parse(parser); + } catch (Script.ParseException pe) { + throw new AlertsSettingsException("could not parse [script] transform", pe); + } + return new ScriptTransform(script, scriptService); + } + } +} diff --git a/src/test/java/org/elasticsearch/alerts/transform/ScriptTransformTests.java b/src/test/java/org/elasticsearch/alerts/transform/ScriptTransformTests.java new file mode 100644 index 00000000000..87b0f47e0d2 --- /dev/null +++ b/src/test/java/org/elasticsearch/alerts/transform/ScriptTransformTests.java @@ -0,0 +1,100 @@ +/* + * 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.transform; + +import org.elasticsearch.alerts.ExecutionContext; +import org.elasticsearch.alerts.Payload; +import org.elasticsearch.alerts.support.Script; +import org.elasticsearch.alerts.support.Variables; +import org.elasticsearch.alerts.support.init.proxy.ScriptServiceProxy; +import org.elasticsearch.common.collect.ImmutableMap; +import org.elasticsearch.common.joda.time.DateTime; +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.Test; + +import java.util.Collections; +import java.util.Map; + +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * + */ +public class ScriptTransformTests extends ElasticsearchTestCase { + + @Test + public void testApply() throws Exception { + ScriptServiceProxy service = mock(ScriptServiceProxy.class); + ScriptService.ScriptType type = randomFrom(ScriptService.ScriptType.values()); + Map params = Collections.emptyMap(); + Script script = new Script("_script", type, "_lang", params); + ScriptTransform transform = new ScriptTransform(script, service); + + DateTime now = new DateTime(); + ExecutionContext ctx = mock(ExecutionContext.class); + when(ctx.scheduledTime()).thenReturn(now); + when(ctx.fireTime()).thenReturn(now); + + Payload payload = new Payload.Simple(ImmutableMap.builder().put("key", "value").build()); + + Map model = ImmutableMap.builder() + .put(Variables.PAYLOAD, payload.data()) + .put(Variables.FIRE_TIME, now) + .put(Variables.SCHEDULED_FIRE_TIME, now) + .build(); + + Map transformed = ImmutableMap.builder() + .put("key", "value") + .build(); + + ExecutableScript executable = mock(ExecutableScript.class); + when(executable.run()).thenReturn(transformed); + when(service.executable("_lang", "_script", type, model)).thenReturn(executable); + + Transform.Result result = transform.apply(ctx, payload); + assertThat(result, notNullValue()); + assertThat(result.type(), is(ScriptTransform.TYPE)); + assertThat(result.payload().data(), equalTo(transformed)); + } + + @Test + public void testParser() throws Exception { + ScriptServiceProxy service = mock(ScriptServiceProxy.class); + ScriptService.ScriptType type = randomFrom(ScriptService.ScriptType.values()); + XContentBuilder builder = jsonBuilder().startObject() + .field("script", "_script") + .field("lang", "_lang") + .field("type", type.name()) + .startObject("params").field("key", "value").endObject() + .endObject(); + + XContentParser parser = JsonXContent.jsonXContent.createParser(builder.bytes()); + parser.nextToken(); + ScriptTransform transform = new ScriptTransform.Parser(service).parse(parser); + assertThat(transform.script(), equalTo(new Script("_script", type, "_lang", ImmutableMap.builder().put("key", "value").build()))); + } + + @Test + public void testParser_String() throws Exception { + ScriptServiceProxy service = mock(ScriptServiceProxy.class); + XContentBuilder builder = jsonBuilder().value("_script"); + + XContentParser parser = JsonXContent.jsonXContent.createParser(builder.bytes()); + parser.nextToken(); + ScriptTransform transform = new ScriptTransform.Parser(service).parse(parser); + assertThat(transform.script(), equalTo(new Script("_script", ScriptService.ScriptType.INLINE, ScriptService.DEFAULT_LANG, ImmutableMap.of()))); + } +}