Mustache: Add util functions to render JSON and join array values
This pull request adds two util functions to the Mustache templating engine: - {{#toJson}}my_map{{/toJson}} to render a Map parameter as a JSON string - {{#join}}my_iterable{{/join}} to render any iterable (including arrays) as a comma separated list of values like `1, 2, 3`. It's also possible de change the default delimiter (comma) to something else. closes #18970
This commit is contained in:
parent
b97ea9954c
commit
4820d49120
|
@ -89,6 +89,89 @@ which is rendered as:
|
|||
}
|
||||
------------------------------------------
|
||||
|
||||
|
||||
[float]
|
||||
===== Concatenating array of values
|
||||
|
||||
The `{{#join}}array{{/join}}` function can be used to concatenate the
|
||||
values of an array as a comma delimited string:
|
||||
|
||||
[source,js]
|
||||
------------------------------------------
|
||||
GET /_search/template
|
||||
{
|
||||
"inline": {
|
||||
"query": {
|
||||
"match": {
|
||||
"emails": "{{#join}}emails{{/join}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"params": {
|
||||
"emails": [ "username@email.com", "lastname@email.com" ]
|
||||
}
|
||||
}
|
||||
------------------------------------------
|
||||
|
||||
which is rendered as:
|
||||
|
||||
[source,js]
|
||||
------------------------------------------
|
||||
{
|
||||
"query" : {
|
||||
"match" : {
|
||||
"emails" : "username@email.com,lastname@email.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
------------------------------------------
|
||||
|
||||
The function also accepts a custom delimiter:
|
||||
|
||||
[source,js]
|
||||
------------------------------------------
|
||||
GET /_search/template
|
||||
{
|
||||
"inline": {
|
||||
"query": {
|
||||
"range": {
|
||||
"born": {
|
||||
"gte" : "{{date.min}}",
|
||||
"lte" : "{{date.max}}",
|
||||
"format": "{{#join delimiter='||'}}date.formats{{/join delimiter='||'}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"params": {
|
||||
"date": {
|
||||
"min": "2016",
|
||||
"max": "31/12/2017",
|
||||
"formats": ["dd/MM/yyyy", "yyyy"]
|
||||
}
|
||||
}
|
||||
}
|
||||
------------------------------------------
|
||||
|
||||
which is rendered as:
|
||||
|
||||
[source,js]
|
||||
------------------------------------------
|
||||
{
|
||||
"query" : {
|
||||
"range" : {
|
||||
"born" : {
|
||||
"gte" : "2016",
|
||||
"lte" : "31/12/2017",
|
||||
"format" : "dd/MM/yyyy||yyyy"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
------------------------------------------
|
||||
|
||||
|
||||
[float]
|
||||
===== Default values
|
||||
|
||||
|
@ -140,6 +223,46 @@ for `end`:
|
|||
}
|
||||
------------------------------------------
|
||||
|
||||
[float]
|
||||
===== Converting parameters to JSON
|
||||
|
||||
The `{{toJson}}parameter{{/toJson}}` function can be used to convert parameters
|
||||
like maps and array to their JSON representation:
|
||||
|
||||
[source,js]
|
||||
------------------------------------------
|
||||
{
|
||||
"inline": "{\"query\":{\"bool\":{\"must\": {{#toJson}}clauses{{/toJson}} }}}",
|
||||
"params": {
|
||||
"clauses": [
|
||||
{ "term": "foo" },
|
||||
{ "term": "bar" }
|
||||
]
|
||||
}
|
||||
}
|
||||
------------------------------------------
|
||||
|
||||
which is rendered as:
|
||||
|
||||
[source,js]
|
||||
------------------------------------------
|
||||
{
|
||||
"query" : {
|
||||
"bool" : {
|
||||
"must" : [
|
||||
{
|
||||
"term" : "foo"
|
||||
},
|
||||
{
|
||||
"term" : "bar"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
------------------------------------------
|
||||
|
||||
|
||||
[float]
|
||||
===== Conditional clauses
|
||||
|
||||
|
|
|
@ -0,0 +1,279 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package org.elasticsearch.script.mustache;
|
||||
|
||||
import com.fasterxml.jackson.core.io.JsonStringEncoder;
|
||||
import com.github.mustachejava.Code;
|
||||
import com.github.mustachejava.DefaultMustacheFactory;
|
||||
import com.github.mustachejava.DefaultMustacheVisitor;
|
||||
import com.github.mustachejava.Mustache;
|
||||
import com.github.mustachejava.MustacheException;
|
||||
import com.github.mustachejava.MustacheVisitor;
|
||||
import com.github.mustachejava.TemplateContext;
|
||||
import com.github.mustachejava.codes.IterableCode;
|
||||
import com.github.mustachejava.codes.WriteCode;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentType;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.io.Writer;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class CustomMustacheFactory extends DefaultMustacheFactory {
|
||||
|
||||
private final BiConsumer<String, Writer> encoder;
|
||||
|
||||
public CustomMustacheFactory(boolean escaping) {
|
||||
super();
|
||||
setObjectHandler(new CustomReflectionObjectHandler());
|
||||
if (escaping) {
|
||||
this.encoder = new JsonEscapeEncoder();
|
||||
} else {
|
||||
this.encoder = new NoEscapeEncoder();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(String value, Writer writer) {
|
||||
encoder.accept(value, writer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MustacheVisitor createMustacheVisitor() {
|
||||
return new CustomMustacheVisitor(this);
|
||||
}
|
||||
|
||||
class CustomMustacheVisitor extends DefaultMustacheVisitor {
|
||||
|
||||
public CustomMustacheVisitor(DefaultMustacheFactory df) {
|
||||
super(df);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void iterable(TemplateContext templateContext, String variable, Mustache mustache) {
|
||||
if (ToJsonCode.match(variable)) {
|
||||
list.add(new ToJsonCode(templateContext, df, mustache, variable));
|
||||
} else if (JoinerCode.match(variable)) {
|
||||
list.add(new JoinerCode(templateContext, df, mustache));
|
||||
} else if (CustomJoinerCode.match(variable)) {
|
||||
list.add(new CustomJoinerCode(templateContext, df, mustache, variable));
|
||||
} else {
|
||||
list.add(new IterableCode(templateContext, df, mustache, variable));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for custom Mustache functions
|
||||
*/
|
||||
static abstract class CustomCode extends IterableCode {
|
||||
|
||||
private final String code;
|
||||
|
||||
public CustomCode(TemplateContext tc, DefaultMustacheFactory df, Mustache mustache, String code) {
|
||||
super(tc, df, mustache, extractVariableName(code, mustache, tc));
|
||||
this.code = Objects.requireNonNull(code);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Writer execute(Writer writer, final List<Object> scopes) {
|
||||
Object resolved = get(scopes);
|
||||
writer = handle(writer, createFunction(resolved), scopes);
|
||||
appendText(writer);
|
||||
return writer;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tag(Writer writer, String tag) throws IOException {
|
||||
writer.write(tc.startChars());
|
||||
writer.write(tag);
|
||||
writer.write(code);
|
||||
writer.write(tc.endChars());
|
||||
}
|
||||
|
||||
protected abstract Function<String, String> createFunction(Object resolved);
|
||||
|
||||
/**
|
||||
* At compile time, this function extracts the name of the variable:
|
||||
* {{#toJson}}variable_name{{/toJson}}
|
||||
*/
|
||||
protected static String extractVariableName(String fn, Mustache mustache, TemplateContext tc) {
|
||||
Code[] codes = mustache.getCodes();
|
||||
if (codes == null || codes.length != 1) {
|
||||
throw new MustacheException("Mustache function [" + fn + "] must contain one and only one identifier");
|
||||
}
|
||||
|
||||
try (StringWriter capture = new StringWriter()) {
|
||||
// Variable name is in plain text and has type WriteCode
|
||||
if (codes[0] instanceof WriteCode) {
|
||||
codes[0].execute(capture, Collections.emptyList());
|
||||
return capture.toString();
|
||||
} else {
|
||||
codes[0].identity(capture);
|
||||
return capture.toString();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new MustacheException("Exception while parsing mustache function [" + fn + "] at line " + tc.line(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function renders {@link Iterable} and {@link Map} as their JSON representation
|
||||
*/
|
||||
static class ToJsonCode extends CustomCode {
|
||||
|
||||
private static final String CODE = "toJson";
|
||||
|
||||
public ToJsonCode(TemplateContext tc, DefaultMustacheFactory df, Mustache mustache, String variable) {
|
||||
super(tc, df, mustache, CODE);
|
||||
if (CODE.equalsIgnoreCase(variable) == false) {
|
||||
throw new MustacheException("Mismatch function code [" + CODE + "] cannot be applied to [" + variable + "]");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
protected Function<String, String> createFunction(Object resolved) {
|
||||
return s -> {
|
||||
if (resolved == null) {
|
||||
return null;
|
||||
}
|
||||
try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) {
|
||||
if (resolved == null) {
|
||||
builder.nullValue();
|
||||
} else if (resolved instanceof Iterable) {
|
||||
builder.startArray();
|
||||
for (Object o : (Iterable) resolved) {
|
||||
builder.value(o);
|
||||
}
|
||||
builder.endArray();
|
||||
} else if (resolved instanceof Map) {
|
||||
builder.map((Map<String, ?>) resolved);
|
||||
} else {
|
||||
// Do not handle as JSON
|
||||
return oh.stringify(resolved);
|
||||
}
|
||||
return builder.string();
|
||||
} catch (IOException e) {
|
||||
throw new MustacheException("Failed to convert object to JSON", e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static boolean match(String variable) {
|
||||
return CODE.equalsIgnoreCase(variable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function concatenates the values of an {@link Iterable} using a given delimiter
|
||||
*/
|
||||
static class JoinerCode extends CustomCode {
|
||||
|
||||
protected static final String CODE = "join";
|
||||
private static final String DEFAULT_DELIMITER = ",";
|
||||
|
||||
private final String delimiter;
|
||||
|
||||
public JoinerCode(TemplateContext tc, DefaultMustacheFactory df, Mustache mustache, String delimiter) {
|
||||
super(tc, df, mustache, CODE);
|
||||
this.delimiter = delimiter;
|
||||
}
|
||||
|
||||
public JoinerCode(TemplateContext tc, DefaultMustacheFactory df, Mustache mustache) {
|
||||
this(tc, df, mustache, DEFAULT_DELIMITER);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Function<String, String> createFunction(Object resolved) {
|
||||
return s -> {
|
||||
if (s == null) {
|
||||
return null;
|
||||
} else if (resolved instanceof Iterable) {
|
||||
StringJoiner joiner = new StringJoiner(delimiter);
|
||||
for (Object o : (Iterable) resolved) {
|
||||
joiner.add(oh.stringify(o));
|
||||
}
|
||||
return joiner.toString();
|
||||
}
|
||||
return s;
|
||||
};
|
||||
}
|
||||
|
||||
static boolean match(String variable) {
|
||||
return CODE.equalsIgnoreCase(variable);
|
||||
}
|
||||
}
|
||||
|
||||
static class CustomJoinerCode extends JoinerCode {
|
||||
|
||||
private static final Pattern PATTERN = Pattern.compile("^(?:" + CODE + " delimiter='(.*)')$");
|
||||
|
||||
public CustomJoinerCode(TemplateContext tc, DefaultMustacheFactory df, Mustache mustache, String variable) {
|
||||
super(tc, df, mustache, extractDelimiter(variable));
|
||||
}
|
||||
|
||||
private static String extractDelimiter(String variable) {
|
||||
Matcher matcher = PATTERN.matcher(variable);
|
||||
if (matcher.find()) {
|
||||
return matcher.group(1);
|
||||
}
|
||||
throw new MustacheException("Failed to extract delimiter for join function");
|
||||
}
|
||||
|
||||
static boolean match(String variable) {
|
||||
return PATTERN.matcher(variable).matches();
|
||||
}
|
||||
}
|
||||
|
||||
class NoEscapeEncoder implements BiConsumer<String, Writer> {
|
||||
|
||||
@Override
|
||||
public void accept(String s, Writer writer) {
|
||||
try {
|
||||
writer.write(s);
|
||||
} catch (IOException e) {
|
||||
throw new MustacheException("Failed to encode value: " + s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class JsonEscapeEncoder implements BiConsumer<String, Writer> {
|
||||
|
||||
@Override
|
||||
public void accept(String s, Writer writer) {
|
||||
try {
|
||||
writer.write(JsonStringEncoder.getInstance().quoteAsString(s));
|
||||
} catch (IOException e) {
|
||||
throw new MustacheException("Failed to escape and encode value: " + s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.elasticsearch.script.mustache;
|
||||
|
||||
import com.fasterxml.jackson.core.io.JsonStringEncoder;
|
||||
import com.github.mustachejava.DefaultMustacheFactory;
|
||||
import com.github.mustachejava.MustacheException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
|
||||
/**
|
||||
* A MustacheFactory that does simple JSON escaping.
|
||||
*/
|
||||
final class JsonEscapingMustacheFactory extends DefaultMustacheFactory {
|
||||
|
||||
@Override
|
||||
public void encode(String value, Writer writer) {
|
||||
try {
|
||||
writer.write(JsonStringEncoder.getInstance().quoteAsString(value));
|
||||
} catch (IOException e) {
|
||||
throw new MustacheException("Failed to encode value: " + value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,8 +18,8 @@
|
|||
*/
|
||||
package org.elasticsearch.script.mustache;
|
||||
|
||||
import com.github.mustachejava.DefaultMustacheFactory;
|
||||
import com.github.mustachejava.Mustache;
|
||||
import com.github.mustachejava.MustacheFactory;
|
||||
import org.elasticsearch.SpecialPermission;
|
||||
import org.elasticsearch.common.Nullable;
|
||||
import org.elasticsearch.common.component.AbstractComponent;
|
||||
|
@ -29,8 +29,8 @@ import org.elasticsearch.common.io.stream.BytesStreamOutput;
|
|||
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.GeneralScriptException;
|
||||
import org.elasticsearch.script.ScriptEngineService;
|
||||
import org.elasticsearch.script.SearchScript;
|
||||
import org.elasticsearch.search.lookup.SearchLookup;
|
||||
|
||||
|
@ -89,21 +89,13 @@ public final class MustacheScriptEngineService extends AbstractComponent impleme
|
|||
* */
|
||||
@Override
|
||||
public Object compile(String templateName, String templateSource, Map<String, String> params) {
|
||||
String contentType = params.getOrDefault(CONTENT_TYPE_PARAM, JSON_CONTENT_TYPE);
|
||||
final DefaultMustacheFactory mustacheFactory;
|
||||
switch (contentType){
|
||||
case PLAIN_TEXT_CONTENT_TYPE:
|
||||
mustacheFactory = new NoneEscapingMustacheFactory();
|
||||
break;
|
||||
case JSON_CONTENT_TYPE:
|
||||
default:
|
||||
// assume that the default is json encoding:
|
||||
mustacheFactory = new JsonEscapingMustacheFactory();
|
||||
break;
|
||||
}
|
||||
mustacheFactory.setObjectHandler(new CustomReflectionObjectHandler());
|
||||
final MustacheFactory factory = new CustomMustacheFactory(isJsonEscapingEnabled(params));
|
||||
Reader reader = new FastStringReader(templateSource);
|
||||
return mustacheFactory.compile(reader, "query-template");
|
||||
return factory.compile(reader, "query-template");
|
||||
}
|
||||
|
||||
private boolean isJsonEscapingEnabled(Map<String, String> params) {
|
||||
return JSON_CONTENT_TYPE.equals(params.getOrDefault(CONTENT_TYPE_PARAM, JSON_CONTENT_TYPE));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -168,12 +160,9 @@ public final class MustacheScriptEngineService extends AbstractComponent impleme
|
|||
if (sm != null) {
|
||||
sm.checkPermission(SPECIAL_PERMISSION);
|
||||
}
|
||||
AccessController.doPrivileged(new PrivilegedAction<Void>() {
|
||||
@Override
|
||||
public Void run() {
|
||||
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
|
||||
((Mustache) template.compiled()).execute(writer, vars);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
logger.error("Error running {}", e, template);
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.elasticsearch.script.mustache;
|
||||
|
||||
import com.github.mustachejava.DefaultMustacheFactory;
|
||||
import com.github.mustachejava.MustacheException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
|
||||
/**
|
||||
* A MustacheFactory that does no string escaping.
|
||||
*/
|
||||
final class NoneEscapingMustacheFactory extends DefaultMustacheFactory {
|
||||
|
||||
@Override
|
||||
public void encode(String value, Writer writer) {
|
||||
try {
|
||||
writer.write(value);
|
||||
} catch (IOException e) {
|
||||
throw new MustacheException("Failed to encode value: " + value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@
|
|||
*/
|
||||
package org.elasticsearch.script.mustache;
|
||||
|
||||
import com.github.mustachejava.MustacheFactory;
|
||||
import org.elasticsearch.common.bytes.BytesReference;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.script.CompiledScript;
|
||||
|
@ -39,12 +40,12 @@ import static org.hamcrest.Matchers.equalTo;
|
|||
*/
|
||||
public class MustacheScriptEngineTests extends ESTestCase {
|
||||
private MustacheScriptEngineService qe;
|
||||
private JsonEscapingMustacheFactory escaper;
|
||||
private MustacheFactory factory;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
qe = new MustacheScriptEngineService(Settings.Builder.EMPTY_SETTINGS);
|
||||
escaper = new JsonEscapingMustacheFactory();
|
||||
factory = new CustomMustacheFactory(true);
|
||||
}
|
||||
|
||||
public void testSimpleParameterReplace() {
|
||||
|
@ -75,12 +76,12 @@ public class MustacheScriptEngineTests extends ESTestCase {
|
|||
public void testEscapeJson() throws IOException {
|
||||
{
|
||||
StringWriter writer = new StringWriter();
|
||||
escaper.encode("hello \n world", writer);
|
||||
factory.encode("hello \n world", writer);
|
||||
assertThat(writer.toString(), equalTo("hello \\n world"));
|
||||
}
|
||||
{
|
||||
StringWriter writer = new StringWriter();
|
||||
escaper.encode("\n", writer);
|
||||
factory.encode("\n", writer);
|
||||
assertThat(writer.toString(), equalTo("\\n"));
|
||||
}
|
||||
|
||||
|
@ -135,7 +136,7 @@ public class MustacheScriptEngineTests extends ESTestCase {
|
|||
expect.append(escapedChars[charIndex]);
|
||||
}
|
||||
StringWriter target = new StringWriter();
|
||||
escaper.encode(writer.toString(), target);
|
||||
factory.encode(writer.toString(), target);
|
||||
assertThat(expect.toString(), equalTo(target.toString()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,13 +19,16 @@
|
|||
package org.elasticsearch.script.mustache;
|
||||
|
||||
import com.github.mustachejava.Mustache;
|
||||
import com.github.mustachejava.MustacheException;
|
||||
import org.elasticsearch.common.bytes.BytesReference;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentHelper;
|
||||
import org.elasticsearch.script.CompiledScript;
|
||||
import org.elasticsearch.script.ExecutableScript;
|
||||
import org.elasticsearch.script.ScriptEngineService;
|
||||
import org.elasticsearch.script.ScriptService;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
import org.hamcrest.Matcher;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
@ -38,6 +41,8 @@ import java.util.Set;
|
|||
|
||||
import static java.util.Collections.singleton;
|
||||
import static java.util.Collections.singletonMap;
|
||||
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
|
||||
import static org.elasticsearch.script.ScriptService.ScriptType.INLINE;
|
||||
import static org.elasticsearch.script.mustache.MustacheScriptEngineService.CONTENT_TYPE_PARAM;
|
||||
import static org.elasticsearch.script.mustache.MustacheScriptEngineService.JSON_CONTENT_TYPE;
|
||||
import static org.elasticsearch.script.mustache.MustacheScriptEngineService.PLAIN_TEXT_CONTENT_TYPE;
|
||||
|
@ -45,6 +50,8 @@ import static org.hamcrest.Matchers.both;
|
|||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.isEmptyOrNullString;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
|
||||
public class MustacheTests extends ESTestCase {
|
||||
|
@ -59,7 +66,7 @@ public class MustacheTests extends ESTestCase {
|
|||
Map<String, Object> params = Collections.singletonMap("boost_val", "0.2");
|
||||
|
||||
Mustache mustache = (Mustache) engine.compile(null, template, Collections.emptyMap());
|
||||
CompiledScript compiledScript = new CompiledScript(ScriptService.ScriptType.INLINE, "my-name", "mustache", mustache);
|
||||
CompiledScript compiledScript = new CompiledScript(INLINE, "my-name", "mustache", mustache);
|
||||
ExecutableScript result = engine.executable(compiledScript, params);
|
||||
assertEquals(
|
||||
"Mustache templating broken",
|
||||
|
@ -71,7 +78,7 @@ public class MustacheTests extends ESTestCase {
|
|||
|
||||
public void testArrayAccess() throws Exception {
|
||||
String template = "{{data.0}} {{data.1}}";
|
||||
CompiledScript mustache = new CompiledScript(ScriptService.ScriptType.INLINE, "inline", "mustache", engine.compile(null, template, Collections.emptyMap()));
|
||||
CompiledScript mustache = new CompiledScript(INLINE, "inline", "mustache", engine.compile(null, template, Collections.emptyMap()));
|
||||
Map<String, Object> vars = new HashMap<>();
|
||||
Object data = randomFrom(
|
||||
new String[] { "foo", "bar" },
|
||||
|
@ -97,7 +104,7 @@ public class MustacheTests extends ESTestCase {
|
|||
|
||||
public void testArrayInArrayAccess() throws Exception {
|
||||
String template = "{{data.0.0}} {{data.0.1}}";
|
||||
CompiledScript mustache = new CompiledScript(ScriptService.ScriptType.INLINE, "inline", "mustache", engine.compile(null, template, Collections.emptyMap()));
|
||||
CompiledScript mustache = new CompiledScript(INLINE, "inline", "mustache", engine.compile(null, template, Collections.emptyMap()));
|
||||
Map<String, Object> vars = new HashMap<>();
|
||||
Object data = randomFrom(
|
||||
new String[][] { new String[] { "foo", "bar" }},
|
||||
|
@ -114,7 +121,7 @@ public class MustacheTests extends ESTestCase {
|
|||
|
||||
public void testMapInArrayAccess() throws Exception {
|
||||
String template = "{{data.0.key}} {{data.1.key}}";
|
||||
CompiledScript mustache = new CompiledScript(ScriptService.ScriptType.INLINE, "inline", "mustache", engine.compile(null, template, Collections.emptyMap()));
|
||||
CompiledScript mustache = new CompiledScript(INLINE, "inline", "mustache", engine.compile(null, template, Collections.emptyMap()));
|
||||
Map<String, Object> vars = new HashMap<>();
|
||||
Object data = randomFrom(
|
||||
new Object[] { singletonMap("key", "foo"), singletonMap("key", "bar") },
|
||||
|
@ -142,7 +149,7 @@ public class MustacheTests extends ESTestCase {
|
|||
// json string escaping enabled:
|
||||
Map<String, String> params = randomBoolean() ? Collections.emptyMap() : Collections.singletonMap(CONTENT_TYPE_PARAM, JSON_CONTENT_TYPE);
|
||||
Mustache mustache = (Mustache) engine.compile(null, "{ \"field1\": \"{{value}}\"}", Collections.emptyMap());
|
||||
CompiledScript compiledScript = new CompiledScript(ScriptService.ScriptType.INLINE, "name", "mustache", mustache);
|
||||
CompiledScript compiledScript = new CompiledScript(INLINE, "name", "mustache", mustache);
|
||||
ExecutableScript executableScript = engine.executable(compiledScript, Collections.singletonMap("value", "a \"value\""));
|
||||
BytesReference rawResult = (BytesReference) executableScript.run();
|
||||
String result = rawResult.toUtf8();
|
||||
|
@ -150,7 +157,7 @@ public class MustacheTests extends ESTestCase {
|
|||
|
||||
// json string escaping disabled:
|
||||
mustache = (Mustache) engine.compile(null, "{ \"field1\": \"{{value}}\"}", Collections.singletonMap(CONTENT_TYPE_PARAM, PLAIN_TEXT_CONTENT_TYPE));
|
||||
compiledScript = new CompiledScript(ScriptService.ScriptType.INLINE, "name", "mustache", mustache);
|
||||
compiledScript = new CompiledScript(INLINE, "name", "mustache", mustache);
|
||||
executableScript = engine.executable(compiledScript, Collections.singletonMap("value", "a \"value\""));
|
||||
rawResult = (BytesReference) executableScript.run();
|
||||
result = rawResult.toUtf8();
|
||||
|
@ -162,7 +169,7 @@ public class MustacheTests extends ESTestCase {
|
|||
List<String> randomList = Arrays.asList(generateRandomStringArray(10, 20, false));
|
||||
|
||||
String template = "{{data.array.size}} {{data.list.size}}";
|
||||
CompiledScript mustache = new CompiledScript(ScriptService.ScriptType.INLINE, "inline", "mustache", engine.compile(null, template, Collections.emptyMap()));
|
||||
CompiledScript mustache = new CompiledScript(INLINE, "inline", "mustache", engine.compile(null, template, Collections.emptyMap()));
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("array", randomArrayValues);
|
||||
data.put("list", randomList);
|
||||
|
@ -177,4 +184,205 @@ public class MustacheTests extends ESTestCase {
|
|||
String expectedString = String.format(Locale.ROOT, "%s %s", randomArrayValues.length, randomList.size());
|
||||
assertThat(bytes.toUtf8(), equalTo(expectedString));
|
||||
}
|
||||
|
||||
public void testPrimitiveToJSON() throws Exception {
|
||||
String template = "{{#toJson}}ctx{{/toJson}}";
|
||||
assertScript(template, Collections.singletonMap("ctx", "value"), equalTo("value"));
|
||||
assertScript(template, Collections.singletonMap("ctx", ""), equalTo(""));
|
||||
assertScript(template, Collections.singletonMap("ctx", true), equalTo("true"));
|
||||
assertScript(template, Collections.singletonMap("ctx", 42), equalTo("42"));
|
||||
assertScript(template, Collections.singletonMap("ctx", 42L), equalTo("42"));
|
||||
assertScript(template, Collections.singletonMap("ctx", 42.5f), equalTo("42.5"));
|
||||
assertScript(template, Collections.singletonMap("ctx", null), equalTo(""));
|
||||
|
||||
template = "{{#toJson}}.{{/toJson}}";
|
||||
assertScript(template, Collections.singletonMap("ctx", "value"), equalTo("{\"ctx\":\"value\"}"));
|
||||
assertScript(template, Collections.singletonMap("ctx", ""), equalTo("{\"ctx\":\"\"}"));
|
||||
assertScript(template, Collections.singletonMap("ctx", true), equalTo("{\"ctx\":true}"));
|
||||
assertScript(template, Collections.singletonMap("ctx", 42), equalTo("{\"ctx\":42}"));
|
||||
assertScript(template, Collections.singletonMap("ctx", 42L), equalTo("{\"ctx\":42}"));
|
||||
assertScript(template, Collections.singletonMap("ctx", 42.5f), equalTo("{\"ctx\":42.5}"));
|
||||
assertScript(template, Collections.singletonMap("ctx", null), equalTo("{\"ctx\":null}"));
|
||||
}
|
||||
|
||||
public void testSimpleMapToJSON() throws Exception {
|
||||
Map<String, Object> human0 = new HashMap<>();
|
||||
human0.put("age", 42);
|
||||
human0.put("name", "John Smith");
|
||||
human0.put("height", 1.84);
|
||||
|
||||
Map<String, Object> ctx = Collections.singletonMap("ctx", human0);
|
||||
|
||||
assertScript("{{#toJson}}.{{/toJson}}", ctx, equalTo("{\"ctx\":{\"name\":\"John Smith\",\"age\":42,\"height\":1.84}}"));
|
||||
assertScript("{{#toJson}}ctx{{/toJson}}", ctx, equalTo("{\"name\":\"John Smith\",\"age\":42,\"height\":1.84}"));
|
||||
assertScript("{{#toJson}}ctx.name{{/toJson}}", ctx, equalTo("John Smith"));
|
||||
}
|
||||
|
||||
public void testMultipleMapsToJSON() throws Exception {
|
||||
Map<String, Object> human0 = new HashMap<>();
|
||||
human0.put("age", 42);
|
||||
human0.put("name", "John Smith");
|
||||
human0.put("height", 1.84);
|
||||
|
||||
Map<String, Object> human1 = new HashMap<>();
|
||||
human1.put("age", 27);
|
||||
human1.put("name", "Dave Smith");
|
||||
human1.put("height", 1.71);
|
||||
|
||||
Map<String, Object> humans = new HashMap<>();
|
||||
humans.put("first", human0);
|
||||
humans.put("second", human1);
|
||||
|
||||
Map<String, Object> ctx = Collections.singletonMap("ctx", humans);
|
||||
|
||||
assertScript("{{#toJson}}.{{/toJson}}", ctx,
|
||||
equalTo("{\"ctx\":{\"first\":{\"name\":\"John Smith\",\"age\":42,\"height\":1.84},\"second\":{\"name\":\"Dave Smith\",\"age\":27,\"height\":1.71}}}"));
|
||||
|
||||
assertScript("{{#toJson}}ctx{{/toJson}}", ctx,
|
||||
equalTo("{\"first\":{\"name\":\"John Smith\",\"age\":42,\"height\":1.84},\"second\":{\"name\":\"Dave Smith\",\"age\":27,\"height\":1.71}}"));
|
||||
|
||||
assertScript("{{#toJson}}ctx.first{{/toJson}}", ctx,
|
||||
equalTo("{\"name\":\"John Smith\",\"age\":42,\"height\":1.84}"));
|
||||
|
||||
assertScript("{{#toJson}}ctx.second{{/toJson}}", ctx,
|
||||
equalTo("{\"name\":\"Dave Smith\",\"age\":27,\"height\":1.71}"));
|
||||
}
|
||||
|
||||
public void testSimpleArrayToJSON() throws Exception {
|
||||
String[] array = new String[]{"one", "two", "three"};
|
||||
Map<String, Object> ctx = Collections.singletonMap("array", array);
|
||||
|
||||
assertScript("{{#toJson}}.{{/toJson}}", ctx, equalTo("{\"array\":[\"one\",\"two\",\"three\"]}"));
|
||||
assertScript("{{#toJson}}array{{/toJson}}", ctx, equalTo("[\"one\",\"two\",\"three\"]"));
|
||||
assertScript("{{#toJson}}array.0{{/toJson}}", ctx, equalTo("one"));
|
||||
assertScript("{{#toJson}}array.1{{/toJson}}", ctx, equalTo("two"));
|
||||
assertScript("{{#toJson}}array.2{{/toJson}}", ctx, equalTo("three"));
|
||||
assertScript("{{#toJson}}array.size{{/toJson}}", ctx, equalTo("3"));
|
||||
}
|
||||
|
||||
public void testSimpleListToJSON() throws Exception {
|
||||
List<String> list = Arrays.asList("one", "two", "three");
|
||||
Map<String, Object> ctx = Collections.singletonMap("ctx", list);
|
||||
|
||||
assertScript("{{#toJson}}.{{/toJson}}", ctx, equalTo("{\"ctx\":[\"one\",\"two\",\"three\"]}"));
|
||||
assertScript("{{#toJson}}ctx{{/toJson}}", ctx, equalTo("[\"one\",\"two\",\"three\"]"));
|
||||
assertScript("{{#toJson}}ctx.0{{/toJson}}", ctx, equalTo("one"));
|
||||
assertScript("{{#toJson}}ctx.1{{/toJson}}", ctx, equalTo("two"));
|
||||
assertScript("{{#toJson}}ctx.2{{/toJson}}", ctx, equalTo("three"));
|
||||
assertScript("{{#toJson}}ctx.size{{/toJson}}", ctx, equalTo("3"));
|
||||
}
|
||||
|
||||
public void testsUnsupportedTagsToJson() {
|
||||
MustacheException e = expectThrows(MustacheException.class, () -> compile("{{#toJson}}{{foo}}{{bar}}{{/toJson}}"));
|
||||
assertThat(e.getMessage(), containsString("Mustache function [toJson] must contain one and only one identifier"));
|
||||
|
||||
e = expectThrows(MustacheException.class, () -> compile("{{#toJson}}{{/toJson}}"));
|
||||
assertThat(e.getMessage(), containsString("Mustache function [toJson] must contain one and only one identifier"));
|
||||
}
|
||||
|
||||
public void testEmbeddedToJSON() throws Exception {
|
||||
XContentBuilder builder = jsonBuilder().startObject()
|
||||
.startArray("bulks")
|
||||
.startObject()
|
||||
.field("index", "index-1")
|
||||
.field("type", "type-1")
|
||||
.field("id", 1)
|
||||
.endObject()
|
||||
.startObject()
|
||||
.field("index", "index-2")
|
||||
.field("type", "type-2")
|
||||
.field("id", 2)
|
||||
.endObject()
|
||||
.endArray()
|
||||
.endObject();
|
||||
|
||||
Map<String, Object> ctx = Collections.singletonMap("ctx", XContentHelper.convertToMap(builder.bytes(), false).v2());
|
||||
|
||||
assertScript("{{#ctx.bulks}}{{#toJson}}.{{/toJson}}{{/ctx.bulks}}", ctx,
|
||||
equalTo("{\"index\":\"index-1\",\"id\":1,\"type\":\"type-1\"}{\"index\":\"index-2\",\"id\":2,\"type\":\"type-2\"}"));
|
||||
|
||||
assertScript("{{#ctx.bulks}}<{{#toJson}}id{{/toJson}}>{{/ctx.bulks}}", ctx,
|
||||
equalTo("<1><2>"));
|
||||
}
|
||||
|
||||
public void testSimpleArrayJoin() throws Exception {
|
||||
String template = "{{#join}}array{{/join}}";
|
||||
assertScript(template, Collections.singletonMap("array", new String[]{"one", "two", "three"}), equalTo("one,two,three"));
|
||||
assertScript(template, Collections.singletonMap("array", new int[]{1, 2, 3}), equalTo("1,2,3"));
|
||||
assertScript(template, Collections.singletonMap("array", new long[]{1L, 2L, 3L}), equalTo("1,2,3"));
|
||||
assertScript(template, Collections.singletonMap("array", new double[]{1.5, 2.5, 3.5}), equalTo("1.5,2.5,3.5"));
|
||||
assertScript(template, Collections.singletonMap("array", new boolean[]{true, false, true}), equalTo("true,false,true"));
|
||||
assertScript(template, Collections.singletonMap("array", new boolean[]{true, false, true}), equalTo("true,false,true"));
|
||||
}
|
||||
|
||||
public void testEmbeddedArrayJoin() throws Exception {
|
||||
XContentBuilder builder = jsonBuilder().startObject()
|
||||
.startArray("people")
|
||||
.startObject()
|
||||
.field("name", "John Smith")
|
||||
.startArray("emails")
|
||||
.value("john@smith.com")
|
||||
.value("john.smith@email.com")
|
||||
.value("jsmith@email.com")
|
||||
.endArray()
|
||||
.endObject()
|
||||
.startObject()
|
||||
.field("name", "John Doe")
|
||||
.startArray("emails")
|
||||
.value("john@doe.com")
|
||||
.value("john.doe@email.com")
|
||||
.value("jdoe@email.com")
|
||||
.endArray()
|
||||
.endObject()
|
||||
.endArray()
|
||||
.endObject();
|
||||
|
||||
Map<String, Object> ctx = Collections.singletonMap("ctx", XContentHelper.convertToMap(builder.bytes(), false).v2());
|
||||
|
||||
assertScript("{{#join}}ctx.people.0.emails{{/join}}", ctx,
|
||||
equalTo("john@smith.com,john.smith@email.com,jsmith@email.com"));
|
||||
|
||||
assertScript("{{#join}}ctx.people.1.emails{{/join}}", ctx,
|
||||
equalTo("john@doe.com,john.doe@email.com,jdoe@email.com"));
|
||||
|
||||
assertScript("{{#ctx.people}}to: {{#join}}emails{{/join}};{{/ctx.people}}", ctx,
|
||||
equalTo("to: john@smith.com,john.smith@email.com,jsmith@email.com;to: john@doe.com,john.doe@email.com,jdoe@email.com;"));
|
||||
}
|
||||
|
||||
public void testJoinWithToJson() {
|
||||
Map<String, Object> params = Collections.singletonMap("terms",
|
||||
Arrays.asList(singletonMap("term", "foo"), singletonMap("term", "bar")));
|
||||
|
||||
assertScript("{{#join}}{{#toJson}}terms{{/toJson}}{{/join}}", params,
|
||||
equalTo("[{\"term\":\"foo\"},{\"term\":\"bar\"}]"));
|
||||
}
|
||||
|
||||
public void testsUnsupportedTagsJoin() {
|
||||
MustacheException e = expectThrows(MustacheException.class, () -> compile("{{#join}}{{/join}}"));
|
||||
assertThat(e.getMessage(), containsString("Mustache function [join] must contain one and only one identifier"));
|
||||
|
||||
e = expectThrows(MustacheException.class, () -> compile("{{#join delimiter='a'}}{{/join delimiter='b'}}"));
|
||||
assertThat(e.getMessage(), containsString("Mismatched start/end tags"));
|
||||
}
|
||||
|
||||
public void testJoinWithCustomDelimiter() {
|
||||
Map<String, Object> params = Collections.singletonMap("params", Arrays.asList(1, 2, 3, 4));
|
||||
|
||||
assertScript("{{#join delimiter=''}}params{{/join delimiter=''}}", params, equalTo("1234"));
|
||||
assertScript("{{#join delimiter=','}}params{{/join delimiter=','}}", params, equalTo("1,2,3,4"));
|
||||
assertScript("{{#join delimiter='/'}}params{{/join delimiter='/'}}", params, equalTo("1/2/3/4"));
|
||||
assertScript("{{#join delimiter=' and '}}params{{/join delimiter=' and '}}", params, equalTo("1 and 2 and 3 and 4"));
|
||||
}
|
||||
|
||||
private void assertScript(String script, Map<String, Object> vars, Matcher<Object> matcher) {
|
||||
Object result = engine.executable(new CompiledScript(INLINE, "inline", "mustache", compile(script)), vars).run();
|
||||
assertThat(result, notNullValue());
|
||||
assertThat(result, instanceOf(BytesReference.class));
|
||||
assertThat(((BytesReference) result).toUtf8(), matcher);
|
||||
}
|
||||
|
||||
private Object compile(String script) {
|
||||
assertThat("cannot compile null or empty script", script, not(isEmptyOrNullString()));
|
||||
return engine.compile(null, script, Collections.emptyMap());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue