Add array access support for mustache templates
The default mustache template that is supported by elasticsearch doesn't support array/list access. This poses a real limitation for watcher as with `search` input, the hits are returned as an array/list. To bypass this limitation, an extra (tedious) step is required in order to transform the hits to a data structure that is supported by mustache. This commit adds a new mustache script engine - `xmustache` to elasticsearch that supports array/list access in the form of `array.X` where `X` is the index into the array/list. This enables accessing the search results without using a transform. The following example will fetch the `"key"` field of the 3rd hit in the search result: `ctx.payload.hits.hits.3._source.key`. This array/list support will be added to elasticsearch, but it'll only be available in later versions. For now, the default template in watcher will therefore be `xmustache`. Added docs for templates Fixes elastic/elasticsearch#230 Original commit: elastic/x-pack-elasticsearch@b09cad7f8b
This commit is contained in:
parent
02ba76fe21
commit
54fddac93f
|
@ -10,6 +10,7 @@ import org.elasticsearch.common.component.AbstractComponent;
|
|||
import org.elasticsearch.common.inject.Inject;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.script.ExecutableScript;
|
||||
import org.elasticsearch.script.mustache.MustacheScriptEngineService;
|
||||
import org.elasticsearch.watcher.support.init.proxy.ScriptServiceProxy;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
@ -33,7 +34,7 @@ public class MustacheTemplateEngine extends AbstractComponent implements Templat
|
|||
Map<String, Object> mergedModel = new HashMap<>();
|
||||
mergedModel.putAll(template.getParams());
|
||||
mergedModel.putAll(model);
|
||||
ExecutableScript executable = service.executable("mustache", template.getText(), template.getType(), mergedModel);
|
||||
ExecutableScript executable = service.executable(MustacheScriptEngineService.NAME, template.getText(), template.getType(), mergedModel);
|
||||
Object result = executable.run();
|
||||
if (result instanceof BytesReference) {
|
||||
return ((BytesReference) result).toUtf8();
|
||||
|
|
|
@ -6,15 +6,27 @@
|
|||
package org.elasticsearch.watcher.support.template;
|
||||
|
||||
import org.elasticsearch.common.inject.AbstractModule;
|
||||
import org.elasticsearch.common.inject.Module;
|
||||
import org.elasticsearch.common.inject.PreProcessModule;
|
||||
import org.elasticsearch.script.ScriptModule;
|
||||
import org.elasticsearch.watcher.support.template.xmustache.XMustacheScriptEngineService;
|
||||
import org.elasticsearch.watcher.support.template.xmustache.XMustacheTemplateEngine;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class TemplateModule extends AbstractModule {
|
||||
public class TemplateModule extends AbstractModule implements PreProcessModule {
|
||||
|
||||
@Override
|
||||
public void processModule(Module module) {
|
||||
if (module instanceof ScriptModule) {
|
||||
((ScriptModule) module).addScriptEngine(XMustacheScriptEngineService.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(MustacheTemplateEngine.class).asEagerSingleton();
|
||||
bind(TemplateEngine.class).to(MustacheTemplateEngine.class);
|
||||
bind(XMustacheTemplateEngine.class).asEagerSingleton();
|
||||
bind(TemplateEngine.class).to(XMustacheTemplateEngine.class);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,202 @@
|
|||
/*
|
||||
* 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.watcher.support.template.xmustache;
|
||||
|
||||
import org.elasticsearch.common.collect.Iterables;
|
||||
import org.elasticsearch.common.mustache.DefaultMustacheFactory;
|
||||
import org.elasticsearch.common.mustache.MustacheException;
|
||||
import org.elasticsearch.common.mustache.reflect.ReflectionObjectHandler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* An extension to elasticsearch's {@code JsonEscapingMustacheFactory} that on top of applying json
|
||||
* escapes it also enables support for navigating arrays using `array.X` notation (where `X` is the index
|
||||
* of the element in the array).
|
||||
*/
|
||||
public class XMustacheFactory extends DefaultMustacheFactory {
|
||||
|
||||
public XMustacheFactory() {
|
||||
setObjectHandler(new ReflectionObjectHandler() {
|
||||
@Override
|
||||
public Object coerce(Object object) {
|
||||
if (object != null) {
|
||||
if (object.getClass().isArray()) {
|
||||
return new ArrayMap(object);
|
||||
} else if (object instanceof Collection) {
|
||||
return new CollectionMap((Collection) object);
|
||||
}
|
||||
}
|
||||
return super.coerce(object);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(String value, Writer writer) {
|
||||
try {
|
||||
escape(value, writer);
|
||||
} catch (IOException e) {
|
||||
throw new MustacheException("Failed to encode value: " + value);
|
||||
}
|
||||
}
|
||||
|
||||
public static Writer escape(String value, Writer writer) throws IOException {
|
||||
for (int i = 0; i < value.length(); i++) {
|
||||
final char character = value.charAt(i);
|
||||
if (isEscapeChar(character)) {
|
||||
writer.write('\\');
|
||||
}
|
||||
writer.write(character);
|
||||
}
|
||||
return writer;
|
||||
}
|
||||
|
||||
public static boolean isEscapeChar(char c) {
|
||||
switch (c) {
|
||||
case '\b':
|
||||
case '\f':
|
||||
case '\n':
|
||||
case '\r':
|
||||
case '"':
|
||||
case '\\':
|
||||
case '\u000B': // vertical tab
|
||||
case '\t':
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
static class ArrayMap extends AbstractMap<Object, Object> implements Iterable<Object> {
|
||||
|
||||
private final Object array;
|
||||
|
||||
public ArrayMap(Object array) {
|
||||
this.array = array;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object get(Object key) {
|
||||
if (key instanceof Number) {
|
||||
return Array.get(array, ((Number) key).intValue());
|
||||
}
|
||||
try {
|
||||
int index = Integer.parseInt(key.toString());
|
||||
return Array.get(array, index);
|
||||
} catch (NumberFormatException nfe) {
|
||||
// if it's not a number it is as if the key doesn't exist
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsKey(Object key) {
|
||||
return get(key) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Entry<Object, Object>> entrySet() {
|
||||
int length = Array.getLength(array);
|
||||
Map<Object, Object> map = new HashMap<>(length);
|
||||
for (int i = 0; i < length; i++) {
|
||||
map.put(i, Array.get(array, i));
|
||||
}
|
||||
return map.entrySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator over a set of elements of type T.
|
||||
*
|
||||
* @return an Iterator.
|
||||
*/
|
||||
@Override
|
||||
public Iterator<Object> iterator() {
|
||||
return new Iter(array);
|
||||
}
|
||||
|
||||
static class Iter implements Iterator<Object> {
|
||||
|
||||
private final Object array;
|
||||
private final int length;
|
||||
private int index;
|
||||
|
||||
public Iter(Object array) {
|
||||
this.array = array;
|
||||
this.length = Array.getLength(array);
|
||||
this.index = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return index < length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object next() {
|
||||
return Array.get(array, index++);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
throw new UnsupportedOperationException("array iterator does not support removing elements");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class CollectionMap extends AbstractMap<Object, Object> implements Iterable<Object> {
|
||||
|
||||
private final Collection col;
|
||||
|
||||
public CollectionMap(Collection col) {
|
||||
this.col = col;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object get(Object key) {
|
||||
if (key instanceof Number) {
|
||||
return Iterables.get(col, ((Number) key).intValue());
|
||||
}
|
||||
try {
|
||||
int index = Integer.parseInt(key.toString());
|
||||
return Iterables.get(col, index);
|
||||
} catch (NumberFormatException nfe) {
|
||||
// if it's not a number it is as if the key doesn't exist
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsKey(Object key) {
|
||||
return get(key) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Entry<Object, Object>> entrySet() {
|
||||
Map<Object, Object> map = new HashMap<>(col.size());
|
||||
int i = 0;
|
||||
for (Object item : col) {
|
||||
map.put(i++, item);
|
||||
}
|
||||
return map.entrySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator over a set of elements of type T.
|
||||
*
|
||||
* @return an Iterator.
|
||||
*/
|
||||
@Override
|
||||
public Iterator<Object> iterator() {
|
||||
return col.iterator();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
/*
|
||||
* 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.watcher.support.template.xmustache;
|
||||
|
||||
import org.elasticsearch.common.Nullable;
|
||||
import org.elasticsearch.common.component.AbstractComponent;
|
||||
import org.elasticsearch.common.inject.Inject;
|
||||
import org.elasticsearch.common.io.FastStringReader;
|
||||
import org.elasticsearch.common.io.UTF8StreamWriter;
|
||||
import org.elasticsearch.common.io.stream.BytesStreamOutput;
|
||||
import org.elasticsearch.common.mustache.Mustache;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.script.CompiledScript;
|
||||
import org.elasticsearch.script.ExecutableScript;
|
||||
import org.elasticsearch.script.ScriptEngineService;
|
||||
import org.elasticsearch.script.SearchScript;
|
||||
import org.elasticsearch.search.lookup.SearchLookup;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class XMustacheScriptEngineService extends AbstractComponent implements ScriptEngineService {
|
||||
|
||||
public static final String NAME = "xmustache";
|
||||
|
||||
/**
|
||||
* @param settings automatically wired by Guice.
|
||||
* */
|
||||
@Inject
|
||||
public XMustacheScriptEngineService(Settings settings) {
|
||||
super(settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile a template string to (in this case) a Mustache object than can
|
||||
* later be re-used for execution to fill in missing parameter values.
|
||||
*
|
||||
* @param template
|
||||
* a string representing the template to compile.
|
||||
* @return a compiled template object for later execution.
|
||||
* */
|
||||
@Override
|
||||
public Object compile(String template) {
|
||||
/** Factory to generate Mustache objects from. */
|
||||
return (new XMustacheFactory()).compile(new FastStringReader(template), "query-template");
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a compiled template object (as retrieved from the compile method)
|
||||
* and fill potential place holders with the variables given.
|
||||
*
|
||||
* @param template
|
||||
* compiled template object.
|
||||
* @param vars
|
||||
* map of variables to use during substitution.
|
||||
*
|
||||
* @return the processed string with all given variables substitued.
|
||||
* */
|
||||
@Override
|
||||
public Object execute(Object template, Map<String, Object> vars) {
|
||||
BytesStreamOutput result = new BytesStreamOutput();
|
||||
UTF8StreamWriter writer = utf8StreamWriter().setOutput(result);
|
||||
((Mustache) template).execute(writer, vars);
|
||||
try {
|
||||
writer.flush();
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not execute query template (failed to flush writer): ", e);
|
||||
} finally {
|
||||
try {
|
||||
writer.close();
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not execute query template (failed to close writer): ", e);
|
||||
}
|
||||
}
|
||||
return result.bytes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] types() {
|
||||
return new String[] { NAME };
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] extensions() {
|
||||
return new String[] { NAME };
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean sandboxed() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExecutableScript executable(Object mustache,
|
||||
@Nullable Map<String, Object> vars) {
|
||||
return new MustacheExecutableScript((Mustache) mustache, vars);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchScript search(Object compiledScript, SearchLookup lookup,
|
||||
@Nullable Map<String, Object> vars) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object unwrap(Object value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// Nothing to do here
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scriptRemoved(CompiledScript script) {
|
||||
// Nothing to do here
|
||||
}
|
||||
|
||||
/**
|
||||
* Used at query execution time by script service in order to execute a query template.
|
||||
* */
|
||||
private class MustacheExecutableScript implements ExecutableScript {
|
||||
/** Compiled template object. */
|
||||
private Mustache mustache;
|
||||
/** Parameters to fill above object with. */
|
||||
private Map<String, Object> vars;
|
||||
|
||||
/**
|
||||
* @param mustache the compiled template object
|
||||
* @param vars the parameters to fill above object with
|
||||
**/
|
||||
public MustacheExecutableScript(Mustache mustache,
|
||||
Map<String, Object> vars) {
|
||||
this.mustache = mustache;
|
||||
this.vars = vars == null ? Collections.<String, Object>emptyMap() : vars;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNextVar(String name, Object value) {
|
||||
this.vars.put(name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object run() {
|
||||
BytesStreamOutput result = new BytesStreamOutput();
|
||||
UTF8StreamWriter writer = utf8StreamWriter().setOutput(result);
|
||||
mustache.execute(writer, vars);
|
||||
try {
|
||||
writer.flush();
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not execute query template (failed to flush writer): ", e);
|
||||
} finally {
|
||||
try {
|
||||
writer.close();
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not execute query template (failed to close writer): ", e);
|
||||
}
|
||||
}
|
||||
return result.bytes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object unwrap(Object value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/** Thread local UTF8StreamWriter to store template execution results in, thread local to save object creation.*/
|
||||
private static ThreadLocal<SoftReference<UTF8StreamWriter>> utf8StreamWriter = new ThreadLocal<>();
|
||||
|
||||
/** If exists, reset and return, otherwise create, reset and return a writer.*/
|
||||
private static UTF8StreamWriter utf8StreamWriter() {
|
||||
SoftReference<UTF8StreamWriter> ref = utf8StreamWriter.get();
|
||||
UTF8StreamWriter writer = (ref == null) ? null : ref.get();
|
||||
if (writer == null) {
|
||||
writer = new UTF8StreamWriter(1024 * 4);
|
||||
utf8StreamWriter.set(new SoftReference<>(writer));
|
||||
}
|
||||
writer.reset();
|
||||
return writer;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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.watcher.support.template.xmustache;
|
||||
|
||||
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.script.ExecutableScript;
|
||||
import org.elasticsearch.watcher.support.init.proxy.ScriptServiceProxy;
|
||||
import org.elasticsearch.watcher.support.template.Template;
|
||||
import org.elasticsearch.watcher.support.template.TemplateEngine;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class XMustacheTemplateEngine extends AbstractComponent implements TemplateEngine {
|
||||
|
||||
private final ScriptServiceProxy service;
|
||||
|
||||
@Inject
|
||||
public XMustacheTemplateEngine(Settings settings, ScriptServiceProxy service) {
|
||||
super(settings);
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String render(Template template, Map<String, Object> model) {
|
||||
Map<String, Object> mergedModel = new HashMap<>();
|
||||
mergedModel.putAll(template.getParams());
|
||||
mergedModel.putAll(model);
|
||||
ExecutableScript executable = service.executable(XMustacheScriptEngineService.NAME, template.getText(), template.getType(), mergedModel);
|
||||
Object result = executable.run();
|
||||
if (result instanceof BytesReference) {
|
||||
return ((BytesReference) result).toUtf8();
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
}
|
|
@ -20,7 +20,6 @@ import org.elasticsearch.env.Environment;
|
|||
import org.elasticsearch.node.settings.NodeSettingsService;
|
||||
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;
|
||||
|
@ -35,9 +34,10 @@ import org.elasticsearch.watcher.support.http.auth.HttpAuth;
|
|||
import org.elasticsearch.watcher.support.http.auth.HttpAuthRegistry;
|
||||
import org.elasticsearch.watcher.support.init.proxy.ClientProxy;
|
||||
import org.elasticsearch.watcher.support.init.proxy.ScriptServiceProxy;
|
||||
import org.elasticsearch.watcher.support.template.MustacheTemplateEngine;
|
||||
import org.elasticsearch.watcher.support.template.Template;
|
||||
import org.elasticsearch.watcher.support.template.TemplateEngine;
|
||||
import org.elasticsearch.watcher.support.template.xmustache.XMustacheScriptEngineService;
|
||||
import org.elasticsearch.watcher.support.template.xmustache.XMustacheTemplateEngine;
|
||||
import org.elasticsearch.watcher.test.WatcherTestUtils;
|
||||
import org.elasticsearch.watcher.trigger.schedule.ScheduleTriggerEvent;
|
||||
import org.elasticsearch.watcher.watch.Payload;
|
||||
|
@ -83,11 +83,11 @@ public class WebhookActionTests extends ElasticsearchTestCase {
|
|||
public void init() throws IOException {
|
||||
tp = new ThreadPool(ThreadPool.Names.SAME);
|
||||
Settings settings = ImmutableSettings.EMPTY;
|
||||
MustacheScriptEngineService mustacheScriptEngineService = new MustacheScriptEngineService(settings);
|
||||
XMustacheScriptEngineService mustacheScriptEngineService = new XMustacheScriptEngineService(settings);
|
||||
Set<ScriptEngineService> engineServiceSet = new HashSet<>();
|
||||
engineServiceSet.add(mustacheScriptEngineService);
|
||||
scriptService = ScriptServiceProxy.of(new ScriptService(settings, new Environment(), engineServiceSet, new ResourceWatcherService(settings, tp), new NodeSettingsService(settings)));
|
||||
templateEngine = new MustacheTemplateEngine(settings, scriptService);
|
||||
templateEngine = new XMustacheTemplateEngine(settings, scriptService);
|
||||
testBody = new Template(TEST_BODY_STRING );
|
||||
testPath = new Template(TEST_PATH_STRING);
|
||||
authRegistry = new HttpAuthRegistry(ImmutableMap.of("basic", (HttpAuth.Parser) new BasicAuth.Parser()));
|
||||
|
|
|
@ -15,6 +15,7 @@ import org.elasticsearch.script.ExecutableScript;
|
|||
import org.elasticsearch.script.ScriptService;
|
||||
import org.elasticsearch.test.ElasticsearchTestCase;
|
||||
import org.elasticsearch.watcher.support.init.proxy.ScriptServiceProxy;
|
||||
import org.elasticsearch.watcher.support.template.xmustache.XMustacheTemplateEngine;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
|
@ -33,13 +34,13 @@ public class TemplateTests extends ElasticsearchTestCase {
|
|||
private ScriptServiceProxy proxy;
|
||||
private TemplateEngine engine;
|
||||
private ExecutableScript script;
|
||||
private final String lang = "mustache";
|
||||
private final String lang = "xmustache";
|
||||
|
||||
@Before
|
||||
public void init() throws Exception {
|
||||
proxy = mock(ScriptServiceProxy.class);
|
||||
script = mock(ExecutableScript.class);
|
||||
engine = new MustacheTemplateEngine(ImmutableSettings.EMPTY, proxy);
|
||||
engine = new XMustacheTemplateEngine(ImmutableSettings.EMPTY, proxy);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* 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.watcher.support.template.xmustache;
|
||||
|
||||
import com.carrotsearch.randomizedtesting.generators.RandomPicks;
|
||||
import org.elasticsearch.common.bytes.BytesReference;
|
||||
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||
import org.elasticsearch.test.ElasticsearchTestCase;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class XMustacheScriptEngineTests extends ElasticsearchTestCase {
|
||||
|
||||
private XMustacheScriptEngineService engine;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
engine = new XMustacheScriptEngineService(ImmutableSettings.Builder.EMPTY_SETTINGS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimpleParameterReplace() {
|
||||
{
|
||||
String template = "GET _search {\"query\": " + "{\"boosting\": {" + "\"positive\": {\"match\": {\"body\": \"gift\"}},"
|
||||
+ "\"negative\": {\"term\": {\"body\": {\"value\": \"solr\"}" + "}}, \"negative_boost\": {{boost_val}} } }}";
|
||||
Map<String, Object> vars = new HashMap<>();
|
||||
vars.put("boost_val", "0.3");
|
||||
BytesReference o = (BytesReference) engine.execute(engine.compile(template), vars);
|
||||
assertEquals("GET _search {\"query\": {\"boosting\": {\"positive\": {\"match\": {\"body\": \"gift\"}},"
|
||||
+ "\"negative\": {\"term\": {\"body\": {\"value\": \"solr\"}}}, \"negative_boost\": 0.3 } }}",
|
||||
new String(o.toBytes(), Charset.forName("UTF-8")));
|
||||
}
|
||||
{
|
||||
String template = "GET _search {\"query\": " + "{\"boosting\": {" + "\"positive\": {\"match\": {\"body\": \"gift\"}},"
|
||||
+ "\"negative\": {\"term\": {\"body\": {\"value\": \"{{body_val}}\"}" + "}}, \"negative_boost\": {{boost_val}} } }}";
|
||||
Map<String, Object> vars = new HashMap<>();
|
||||
vars.put("boost_val", "0.3");
|
||||
vars.put("body_val", "\"quick brown\"");
|
||||
BytesReference o = (BytesReference) engine.execute(engine.compile(template), vars);
|
||||
assertEquals("GET _search {\"query\": {\"boosting\": {\"positive\": {\"match\": {\"body\": \"gift\"}},"
|
||||
+ "\"negative\": {\"term\": {\"body\": {\"value\": \"\\\"quick brown\\\"\"}}}, \"negative_boost\": 0.3 } }}",
|
||||
new String(o.toBytes(), Charset.forName("UTF-8")));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEscapeJson() throws IOException {
|
||||
{
|
||||
StringWriter writer = new StringWriter();
|
||||
XMustacheFactory.escape("hello \n world", writer);
|
||||
assertThat(writer.toString(), equalTo("hello \\\n world"));
|
||||
}
|
||||
{
|
||||
StringWriter writer = new StringWriter();
|
||||
XMustacheFactory.escape("\n", writer);
|
||||
assertThat(writer.toString(), equalTo("\\\n"));
|
||||
}
|
||||
|
||||
Character[] specialChars = new Character[]{'\f', '\n', '\r', '"', '\\', (char) 11, '\t', '\b' };
|
||||
int iters = scaledRandomIntBetween(100, 1000);
|
||||
for (int i = 0; i < iters; i++) {
|
||||
int rounds = scaledRandomIntBetween(1, 20);
|
||||
StringWriter escaped = new StringWriter();
|
||||
StringWriter writer = new StringWriter();
|
||||
for (int j = 0; j < rounds; j++) {
|
||||
String s = getChars();
|
||||
writer.write(s);
|
||||
escaped.write(s);
|
||||
char c = RandomPicks.randomFrom(getRandom(), specialChars);
|
||||
writer.append(c);
|
||||
escaped.append('\\');
|
||||
escaped.append(c);
|
||||
}
|
||||
StringWriter target = new StringWriter();
|
||||
assertThat(escaped.toString(), equalTo(XMustacheFactory.escape(writer.toString(), target).toString()));
|
||||
}
|
||||
}
|
||||
|
||||
private String getChars() {
|
||||
String string = randomRealisticUnicodeOfCodepointLengthBetween(0, 10);
|
||||
for (int i = 0; i < string.length(); i++) {
|
||||
if (XMustacheFactory.isEscapeChar(string.charAt(i))) {
|
||||
return string.substring(0, i);
|
||||
}
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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.watcher.support.template.xmustache;
|
||||
|
||||
import com.carrotsearch.ant.tasks.junit4.dependencies.com.google.common.collect.ImmutableList;
|
||||
import com.carrotsearch.randomizedtesting.annotations.Repeat;
|
||||
import org.elasticsearch.common.bytes.BytesReference;
|
||||
import org.elasticsearch.common.collect.ImmutableMap;
|
||||
import org.elasticsearch.common.collect.ImmutableSet;
|
||||
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||
import org.elasticsearch.script.ScriptEngineService;
|
||||
import org.elasticsearch.test.ElasticsearchTestCase;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class XMustacheTests extends ElasticsearchTestCase {
|
||||
|
||||
private ScriptEngineService engine;
|
||||
|
||||
@Before
|
||||
public void init() throws Exception {
|
||||
engine = new XMustacheScriptEngineService(ImmutableSettings.EMPTY);
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 10)
|
||||
public void testArrayAccess() throws Exception {
|
||||
String template = "{{data.0}} {{data.1}}";
|
||||
Object mustache = engine.compile(template);
|
||||
Map<String, Object> vars = new HashMap<>();
|
||||
Object data = randomFrom(
|
||||
new String[] { "foo", "bar" },
|
||||
ImmutableList.of("foo", "bar"),
|
||||
ImmutableSet.of("foo", "bar"));
|
||||
vars.put("data", data);
|
||||
Object output = engine.execute(mustache, vars);
|
||||
assertThat(output, notNullValue());
|
||||
assertThat(output, instanceOf(BytesReference.class));
|
||||
BytesReference bytes = (BytesReference) output;
|
||||
assertThat(bytes.toUtf8(), equalTo("foo bar"));
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 10)
|
||||
public void testArrayInArrayAccess() throws Exception {
|
||||
String template = "{{data.0.0}} {{data.0.1}}";
|
||||
Object mustache = engine.compile(template);
|
||||
Map<String, Object> vars = new HashMap<>();
|
||||
Object data = randomFrom(
|
||||
new String[][] { new String[] {"foo", "bar" }},
|
||||
ImmutableList.of(new String[] {"foo", "bar" }),
|
||||
ImmutableSet.of(new String[] {"foo", "bar" })
|
||||
);
|
||||
vars.put("data", data);
|
||||
Object output = engine.execute(mustache, vars);
|
||||
assertThat(output, notNullValue());
|
||||
assertThat(output, instanceOf(BytesReference.class));
|
||||
BytesReference bytes = (BytesReference) output;
|
||||
assertThat(bytes.toUtf8(), equalTo("foo bar"));
|
||||
}
|
||||
|
||||
@Test @Repeat(iterations = 10)
|
||||
public void testMapInArrayAccess() throws Exception {
|
||||
String template = "{{data.0.key}} {{data.1.key}}";
|
||||
Object mustache = engine.compile(template);
|
||||
Map<String, Object> vars = new HashMap<>();
|
||||
Object data = randomFrom(
|
||||
new Map[] { ImmutableMap.<String, Object>of("key", "foo"), ImmutableMap.<String, Object>of("key", "bar") },
|
||||
ImmutableList.of(ImmutableMap.<String, Object>of("key", "foo"), ImmutableMap.<String, Object>of("key", "bar")),
|
||||
ImmutableSet.of(ImmutableMap.<String, Object>of("key", "foo"), ImmutableMap.<String, Object>of("key", "bar")));
|
||||
vars.put("data", data);
|
||||
Object output = engine.execute(mustache, vars);
|
||||
assertThat(output, notNullValue());
|
||||
assertThat(output, instanceOf(BytesReference.class));
|
||||
BytesReference bytes = (BytesReference) output;
|
||||
assertThat(bytes.toUtf8(), equalTo("foo bar"));
|
||||
}
|
||||
}
|
|
@ -40,7 +40,7 @@ import org.elasticsearch.watcher.support.http.HttpMethod;
|
|||
import org.elasticsearch.watcher.support.http.HttpRequestTemplate;
|
||||
import org.elasticsearch.watcher.support.init.proxy.ClientProxy;
|
||||
import org.elasticsearch.watcher.support.init.proxy.ScriptServiceProxy;
|
||||
import org.elasticsearch.watcher.support.template.MustacheTemplateEngine;
|
||||
import org.elasticsearch.watcher.support.template.xmustache.XMustacheTemplateEngine;
|
||||
import org.elasticsearch.watcher.support.template.Template;
|
||||
import org.elasticsearch.watcher.support.template.TemplateEngine;
|
||||
import org.elasticsearch.watcher.transform.SearchTransform;
|
||||
|
@ -143,7 +143,7 @@ public final class WatcherTestUtils {
|
|||
Template body = new Template("{{ctx.watch_id}} executed with {{ctx.payload.response.hits.total_hits}} hits");
|
||||
httpRequest.body(body);
|
||||
|
||||
TemplateEngine engine = new MustacheTemplateEngine(ImmutableSettings.EMPTY, scriptService);
|
||||
TemplateEngine engine = new XMustacheTemplateEngine(ImmutableSettings.EMPTY, scriptService);
|
||||
|
||||
actions.add(new ActionWrapper("_webhook", new ExecutableWebhookAction(new WebhookAction(httpRequest.build()), logger, httpClient, engine)));
|
||||
|
||||
|
@ -155,7 +155,7 @@ public final class WatcherTestUtils {
|
|||
.to(to)
|
||||
.build();
|
||||
|
||||
TemplateEngine templateEngine = new MustacheTemplateEngine(ImmutableSettings.EMPTY, scriptService);
|
||||
TemplateEngine templateEngine = new XMustacheTemplateEngine(ImmutableSettings.EMPTY, scriptService);
|
||||
|
||||
Authentication auth = new Authentication("testname", "testpassword".toCharArray());
|
||||
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* 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.watcher.test.integration;
|
||||
|
||||
import org.elasticsearch.action.search.SearchRequest;
|
||||
import org.elasticsearch.common.settings.ImmutableSettings;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.watcher.actions.email.service.EmailTemplate;
|
||||
import org.elasticsearch.watcher.actions.email.service.support.EmailServer;
|
||||
import org.elasticsearch.watcher.client.WatcherClient;
|
||||
import org.elasticsearch.watcher.test.AbstractWatcherIntegrationTests;
|
||||
import org.elasticsearch.watcher.trigger.schedule.IntervalSchedule;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import javax.mail.internet.MimeMessage;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.elasticsearch.index.query.QueryBuilders.termQuery;
|
||||
import static org.elasticsearch.search.builder.SearchSourceBuilder.searchSource;
|
||||
import static org.elasticsearch.watcher.actions.ActionBuilders.emailAction;
|
||||
import static org.elasticsearch.watcher.client.WatchSourceBuilders.watchBuilder;
|
||||
import static org.elasticsearch.watcher.condition.ConditionBuilders.scriptCondition;
|
||||
import static org.elasticsearch.watcher.input.InputBuilders.searchInput;
|
||||
import static org.elasticsearch.watcher.test.WatcherTestUtils.newInputSearchRequest;
|
||||
import static org.elasticsearch.watcher.trigger.TriggerBuilders.schedule;
|
||||
import static org.elasticsearch.watcher.trigger.schedule.Schedules.interval;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class EmailActionIntegrationTests extends AbstractWatcherIntegrationTests {
|
||||
|
||||
static final String USERNAME = "_user";
|
||||
static final String PASSWORD = "_passwd";
|
||||
|
||||
private EmailServer server;
|
||||
|
||||
@Before
|
||||
public void init() throws Exception {
|
||||
server = new EmailServer("localhost", 2500, USERNAME, PASSWORD);
|
||||
server.start();
|
||||
}
|
||||
|
||||
@After
|
||||
public void cleanup() throws Exception {
|
||||
server.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Settings nodeSettings(int nodeOrdinal) {
|
||||
return ImmutableSettings.builder()
|
||||
.put(super.nodeSettings(nodeOrdinal))
|
||||
.put("watcher.actions.email.service.account.test.smtp.auth", true)
|
||||
.put("watcher.actions.email.service.account.test.smtp.user", USERNAME)
|
||||
.put("watcher.actions.email.service.account.test.smtp.password", PASSWORD)
|
||||
.put("watcher.actions.email.service.account.test.smtp.port", 2500)
|
||||
.put("watcher.actions.email.service.account.test.smtp.host", "localhost")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArrayAccess() throws Exception {
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
EmailServer.Listener.Handle handle = server.addListener(new EmailServer.Listener() {
|
||||
@Override
|
||||
public void on(MimeMessage message) throws Exception {
|
||||
assertThat(message.getSubject(), equalTo("value"));
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
WatcherClient watcherClient = watcherClient();
|
||||
createIndex("idx");
|
||||
// Have a sample document in the index, the watch is going to evaluate
|
||||
client().prepareIndex("idx", "type").setSource("field", "value").get();
|
||||
refresh();
|
||||
SearchRequest searchRequest = newInputSearchRequest("idx").source(searchSource().query(termQuery("field", "value")));
|
||||
watcherClient.preparePutWatch("_id")
|
||||
.setSource(watchBuilder()
|
||||
.trigger(schedule(interval(5, IntervalSchedule.Interval.Unit.SECONDS)))
|
||||
.input(searchInput(searchRequest))
|
||||
.condition(scriptCondition("ctx.payload.hits.total > 0"))
|
||||
.addAction("_index", emailAction(EmailTemplate.builder().from("_from").to("_to")
|
||||
.subject("{{ctx.payload.hits.hits.0._source.field}}"))))
|
||||
.get();
|
||||
|
||||
if (timeWarped()) {
|
||||
timeWarp().scheduler().trigger("_id");
|
||||
refresh();
|
||||
}
|
||||
|
||||
assertWatchWithMinimumPerformedActionsCount("_id", 1);
|
||||
|
||||
if (!latch.await(5, TimeUnit.SECONDS)) {
|
||||
fail("waited too long for email to be received");
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue