diff --git a/src/main/java/org/elasticsearch/alerts/support/init/InitializingModule.java b/src/main/java/org/elasticsearch/alerts/support/init/InitializingModule.java index 3b44e6e4f79..f956918425f 100644 --- a/src/main/java/org/elasticsearch/alerts/support/init/InitializingModule.java +++ b/src/main/java/org/elasticsearch/alerts/support/init/InitializingModule.java @@ -7,6 +7,7 @@ package org.elasticsearch.alerts.support.init; import org.elasticsearch.alerts.support.init.proxy.ClientProxy; import org.elasticsearch.alerts.support.init.proxy.ScriptServiceProxy; +import org.elasticsearch.alerts.transform.ChainTransform; import org.elasticsearch.common.inject.AbstractModule; import org.elasticsearch.common.inject.multibindings.Multibinder; @@ -24,6 +25,7 @@ public class InitializingModule extends AbstractModule { Multibinder mbinder = Multibinder.newSetBinder(binder(), InitializingService.Initializable.class); mbinder.addBinding().to(ClientProxy.class); mbinder.addBinding().to(ScriptServiceProxy.class); + mbinder.addBinding().to(ChainTransform.Parser.class); bind(InitializingService.class).asEagerSingleton(); } } diff --git a/src/main/java/org/elasticsearch/alerts/transform/ChainTransform.java b/src/main/java/org/elasticsearch/alerts/transform/ChainTransform.java new file mode 100644 index 00000000000..0304f851cda --- /dev/null +++ b/src/main/java/org/elasticsearch/alerts/transform/ChainTransform.java @@ -0,0 +1,113 @@ +/* + * 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.init.InitializingService; +import org.elasticsearch.common.collect.ImmutableList; +import org.elasticsearch.common.inject.Injector; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; + +/** + * + */ +public class ChainTransform extends Transform { + + public static final String TYPE = "chain"; + + private final ImmutableList transforms; + + public ChainTransform(ImmutableList transforms) { + this.transforms = transforms; + } + + @Override + public String type() { + return TYPE; + } + + ImmutableList transforms() { + return transforms; + } + + @Override + public Result apply(ExecutionContext ctx, Payload payload) throws IOException { + for (Transform transform : transforms) { + payload = transform.apply(ctx, payload).payload(); + } + return new Result(TYPE, payload); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startArray(); + for (Transform transform : transforms) { + builder.startObject() + .field(transform.type(), transform) + .endObject(); + } + return builder.endArray(); + } + + public static class Parser implements Transform.Parser, InitializingService.Initializable { + + private TransformRegistry registry; + + // used by guice + public Parser() { + } + + // used for tests + Parser(TransformRegistry registry) { + this.registry = registry; + } + + @Override + public void init(Injector injector) { + this.registry = injector.getInstance(TransformRegistry.class); + } + + @Override + public String type() { + return TYPE; + } + + @Override + public ChainTransform parse(XContentParser parser) throws IOException { + XContentParser.Token token = parser.currentToken(); + if (token != XContentParser.Token.START_ARRAY) { + throw new AlertsSettingsException("could not parse [chain] transform. expected an array of objects, but found [" + token + '}'); + } + + ImmutableList.Builder builder = ImmutableList.builder(); + + String currentFieldName = null; + while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { + if (token != XContentParser.Token.START_OBJECT) { + throw new AlertsSettingsException("could not parse [chain] transform. expected a transform object, but found [" + token + "]"); + } + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token == XContentParser.Token.START_OBJECT) { + builder.add(registry.parse(currentFieldName, parser)); + } else { + throw new AlertsSettingsException("could not parse [chain] transform. expected a transform object, but found [" + token + "]"); + } + } + } + return new ChainTransform(builder.build()); + } + + } + + +} diff --git a/src/test/java/org/elasticsearch/alerts/transform/ChainTransformTests.java b/src/test/java/org/elasticsearch/alerts/transform/ChainTransformTests.java new file mode 100644 index 00000000000..0faa262d7e0 --- /dev/null +++ b/src/test/java/org/elasticsearch/alerts/transform/ChainTransformTests.java @@ -0,0 +1,134 @@ +/* + * 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.common.collect.ImmutableList; +import org.elasticsearch.common.collect.ImmutableMap; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.test.ElasticsearchTestCase; +import org.junit.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.hamcrest.Matchers.*; +import static org.mockito.Mockito.mock; + +/** + * + */ +public class ChainTransformTests extends ElasticsearchTestCase { + + @Test + public void testApply() throws Exception { + ChainTransform transform = new ChainTransform(ImmutableList.of( + new NamedTransform("name1"), + new NamedTransform("name2"), + new NamedTransform("name3"))); + + ExecutionContext ctx = mock(ExecutionContext.class); + Payload payload = new Payload.Simple(new HashMap()); + + Transform.Result result = transform.apply(ctx, payload); + + Map data = result.payload().data(); + assertThat(data, notNullValue()); + assertThat(data, hasKey("names")); + assertThat(data.get("names"), instanceOf(List.class)); + List names = (List) data.get("names"); + assertThat(names, hasSize(3)); + assertThat(names, contains("name1", "name2", "name3")); + } + + @Test + public void testParser() throws Exception { + Map parsers = ImmutableMap.builder() + .put("named", new NamedTransform.Parser()) + .build(); + TransformRegistry registry = new TransformRegistry(parsers); + + ChainTransform.Parser transformParser = new ChainTransform.Parser(registry); + + XContentBuilder builder = jsonBuilder().startArray() + .startObject().startObject("named").field("name", "name1").endObject().endObject() + .startObject().startObject("named").field("name", "name2").endObject().endObject() + .startObject().startObject("named").field("name", "name3").endObject().endObject() + .endArray(); + + XContentParser parser = JsonXContent.jsonXContent.createParser(builder.bytes()); + parser.nextToken(); + ChainTransform transform = transformParser.parse(parser); + assertThat(transform, notNullValue()); + assertThat(transform.transforms(), notNullValue()); + assertThat(transform.transforms(), hasSize(3)); + for (int i = 0; i < transform.transforms().size(); i++) { + assertThat(transform.transforms().get(i), instanceOf(NamedTransform.class)); + assertThat(((NamedTransform) transform.transforms().get(i)).name, is("name" + (i + 1))); + } + } + + private static class NamedTransform extends Transform { + + private final String name; + + public NamedTransform(String name) { + this.name = name; + } + + @Override + public String type() { + return "noop"; + } + + @Override + public Result apply(ExecutionContext ctx, Payload payload) throws IOException { + + Map data = new HashMap<>(payload.data()); + List names = (List) data.get("names"); + if (names == null) { + names = new ArrayList<>(); + data.put("names", names); + } + names.add(name); + return new Result("named", new Payload.Simple(data)); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject().field("name", name).endObject(); + } + + public static class Parser implements Transform.Parser { + + @Override + public String type() { + return "named"; + } + + @Override + public NamedTransform parse(XContentParser parser) throws IOException { + assert parser.currentToken() == XContentParser.Token.START_OBJECT; + XContentParser.Token token = parser.nextToken(); + assert token == XContentParser.Token.FIELD_NAME; // the "name" field + token = parser.nextToken(); + assert token == XContentParser.Token.VALUE_STRING; + String name = parser.text(); + token = parser.nextToken(); + assert token == XContentParser.Token.END_OBJECT; + return new NamedTransform(name); + } + } + + } +}