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]
|
[float]
|
||||||
===== Default values
|
===== 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]
|
[float]
|
||||||
===== Conditional clauses
|
===== 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;
|
package org.elasticsearch.script.mustache;
|
||||||
|
|
||||||
import com.github.mustachejava.DefaultMustacheFactory;
|
|
||||||
import com.github.mustachejava.Mustache;
|
import com.github.mustachejava.Mustache;
|
||||||
|
import com.github.mustachejava.MustacheFactory;
|
||||||
import org.elasticsearch.SpecialPermission;
|
import org.elasticsearch.SpecialPermission;
|
||||||
import org.elasticsearch.common.Nullable;
|
import org.elasticsearch.common.Nullable;
|
||||||
import org.elasticsearch.common.component.AbstractComponent;
|
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.common.settings.Settings;
|
||||||
import org.elasticsearch.script.CompiledScript;
|
import org.elasticsearch.script.CompiledScript;
|
||||||
import org.elasticsearch.script.ExecutableScript;
|
import org.elasticsearch.script.ExecutableScript;
|
||||||
import org.elasticsearch.script.ScriptEngineService;
|
|
||||||
import org.elasticsearch.script.GeneralScriptException;
|
import org.elasticsearch.script.GeneralScriptException;
|
||||||
|
import org.elasticsearch.script.ScriptEngineService;
|
||||||
import org.elasticsearch.script.SearchScript;
|
import org.elasticsearch.script.SearchScript;
|
||||||
import org.elasticsearch.search.lookup.SearchLookup;
|
import org.elasticsearch.search.lookup.SearchLookup;
|
||||||
|
|
||||||
|
@ -89,21 +89,13 @@ public final class MustacheScriptEngineService extends AbstractComponent impleme
|
||||||
* */
|
* */
|
||||||
@Override
|
@Override
|
||||||
public Object compile(String templateName, String templateSource, Map<String, String> params) {
|
public Object compile(String templateName, String templateSource, Map<String, String> params) {
|
||||||
String contentType = params.getOrDefault(CONTENT_TYPE_PARAM, JSON_CONTENT_TYPE);
|
final MustacheFactory factory = new CustomMustacheFactory(isJsonEscapingEnabled(params));
|
||||||
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());
|
|
||||||
Reader reader = new FastStringReader(templateSource);
|
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
|
@Override
|
||||||
|
@ -168,12 +160,9 @@ public final class MustacheScriptEngineService extends AbstractComponent impleme
|
||||||
if (sm != null) {
|
if (sm != null) {
|
||||||
sm.checkPermission(SPECIAL_PERMISSION);
|
sm.checkPermission(SPECIAL_PERMISSION);
|
||||||
}
|
}
|
||||||
AccessController.doPrivileged(new PrivilegedAction<Void>() {
|
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
|
||||||
@Override
|
|
||||||
public Void run() {
|
|
||||||
((Mustache) template.compiled()).execute(writer, vars);
|
((Mustache) template.compiled()).execute(writer, vars);
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Error running {}", e, template);
|
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;
|
package org.elasticsearch.script.mustache;
|
||||||
|
|
||||||
|
import com.github.mustachejava.MustacheFactory;
|
||||||
import org.elasticsearch.common.bytes.BytesReference;
|
import org.elasticsearch.common.bytes.BytesReference;
|
||||||
import org.elasticsearch.common.settings.Settings;
|
import org.elasticsearch.common.settings.Settings;
|
||||||
import org.elasticsearch.script.CompiledScript;
|
import org.elasticsearch.script.CompiledScript;
|
||||||
|
@ -39,12 +40,12 @@ import static org.hamcrest.Matchers.equalTo;
|
||||||
*/
|
*/
|
||||||
public class MustacheScriptEngineTests extends ESTestCase {
|
public class MustacheScriptEngineTests extends ESTestCase {
|
||||||
private MustacheScriptEngineService qe;
|
private MustacheScriptEngineService qe;
|
||||||
private JsonEscapingMustacheFactory escaper;
|
private MustacheFactory factory;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setup() {
|
public void setup() {
|
||||||
qe = new MustacheScriptEngineService(Settings.Builder.EMPTY_SETTINGS);
|
qe = new MustacheScriptEngineService(Settings.Builder.EMPTY_SETTINGS);
|
||||||
escaper = new JsonEscapingMustacheFactory();
|
factory = new CustomMustacheFactory(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testSimpleParameterReplace() {
|
public void testSimpleParameterReplace() {
|
||||||
|
@ -75,12 +76,12 @@ public class MustacheScriptEngineTests extends ESTestCase {
|
||||||
public void testEscapeJson() throws IOException {
|
public void testEscapeJson() throws IOException {
|
||||||
{
|
{
|
||||||
StringWriter writer = new StringWriter();
|
StringWriter writer = new StringWriter();
|
||||||
escaper.encode("hello \n world", writer);
|
factory.encode("hello \n world", writer);
|
||||||
assertThat(writer.toString(), equalTo("hello \\n world"));
|
assertThat(writer.toString(), equalTo("hello \\n world"));
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
StringWriter writer = new StringWriter();
|
StringWriter writer = new StringWriter();
|
||||||
escaper.encode("\n", writer);
|
factory.encode("\n", writer);
|
||||||
assertThat(writer.toString(), equalTo("\\n"));
|
assertThat(writer.toString(), equalTo("\\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,7 +136,7 @@ public class MustacheScriptEngineTests extends ESTestCase {
|
||||||
expect.append(escapedChars[charIndex]);
|
expect.append(escapedChars[charIndex]);
|
||||||
}
|
}
|
||||||
StringWriter target = new StringWriter();
|
StringWriter target = new StringWriter();
|
||||||
escaper.encode(writer.toString(), target);
|
factory.encode(writer.toString(), target);
|
||||||
assertThat(expect.toString(), equalTo(target.toString()));
|
assertThat(expect.toString(), equalTo(target.toString()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,13 +19,16 @@
|
||||||
package org.elasticsearch.script.mustache;
|
package org.elasticsearch.script.mustache;
|
||||||
|
|
||||||
import com.github.mustachejava.Mustache;
|
import com.github.mustachejava.Mustache;
|
||||||
|
import com.github.mustachejava.MustacheException;
|
||||||
import org.elasticsearch.common.bytes.BytesReference;
|
import org.elasticsearch.common.bytes.BytesReference;
|
||||||
import org.elasticsearch.common.settings.Settings;
|
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.CompiledScript;
|
||||||
import org.elasticsearch.script.ExecutableScript;
|
import org.elasticsearch.script.ExecutableScript;
|
||||||
import org.elasticsearch.script.ScriptEngineService;
|
import org.elasticsearch.script.ScriptEngineService;
|
||||||
import org.elasticsearch.script.ScriptService;
|
|
||||||
import org.elasticsearch.test.ESTestCase;
|
import org.elasticsearch.test.ESTestCase;
|
||||||
|
import org.hamcrest.Matcher;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -38,6 +41,8 @@ import java.util.Set;
|
||||||
|
|
||||||
import static java.util.Collections.singleton;
|
import static java.util.Collections.singleton;
|
||||||
import static java.util.Collections.singletonMap;
|
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.CONTENT_TYPE_PARAM;
|
||||||
import static org.elasticsearch.script.mustache.MustacheScriptEngineService.JSON_CONTENT_TYPE;
|
import static org.elasticsearch.script.mustache.MustacheScriptEngineService.JSON_CONTENT_TYPE;
|
||||||
import static org.elasticsearch.script.mustache.MustacheScriptEngineService.PLAIN_TEXT_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.containsString;
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
import static org.hamcrest.Matchers.instanceOf;
|
import static org.hamcrest.Matchers.instanceOf;
|
||||||
|
import static org.hamcrest.Matchers.isEmptyOrNullString;
|
||||||
|
import static org.hamcrest.Matchers.not;
|
||||||
import static org.hamcrest.Matchers.notNullValue;
|
import static org.hamcrest.Matchers.notNullValue;
|
||||||
|
|
||||||
public class MustacheTests extends ESTestCase {
|
public class MustacheTests extends ESTestCase {
|
||||||
|
@ -59,7 +66,7 @@ public class MustacheTests extends ESTestCase {
|
||||||
Map<String, Object> params = Collections.singletonMap("boost_val", "0.2");
|
Map<String, Object> params = Collections.singletonMap("boost_val", "0.2");
|
||||||
|
|
||||||
Mustache mustache = (Mustache) engine.compile(null, template, Collections.emptyMap());
|
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);
|
ExecutableScript result = engine.executable(compiledScript, params);
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"Mustache templating broken",
|
"Mustache templating broken",
|
||||||
|
@ -71,7 +78,7 @@ public class MustacheTests extends ESTestCase {
|
||||||
|
|
||||||
public void testArrayAccess() throws Exception {
|
public void testArrayAccess() throws Exception {
|
||||||
String template = "{{data.0}} {{data.1}}";
|
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<>();
|
Map<String, Object> vars = new HashMap<>();
|
||||||
Object data = randomFrom(
|
Object data = randomFrom(
|
||||||
new String[] { "foo", "bar" },
|
new String[] { "foo", "bar" },
|
||||||
|
@ -97,7 +104,7 @@ public class MustacheTests extends ESTestCase {
|
||||||
|
|
||||||
public void testArrayInArrayAccess() throws Exception {
|
public void testArrayInArrayAccess() throws Exception {
|
||||||
String template = "{{data.0.0}} {{data.0.1}}";
|
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<>();
|
Map<String, Object> vars = new HashMap<>();
|
||||||
Object data = randomFrom(
|
Object data = randomFrom(
|
||||||
new String[][] { new String[] { "foo", "bar" }},
|
new String[][] { new String[] { "foo", "bar" }},
|
||||||
|
@ -114,7 +121,7 @@ public class MustacheTests extends ESTestCase {
|
||||||
|
|
||||||
public void testMapInArrayAccess() throws Exception {
|
public void testMapInArrayAccess() throws Exception {
|
||||||
String template = "{{data.0.key}} {{data.1.key}}";
|
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<>();
|
Map<String, Object> vars = new HashMap<>();
|
||||||
Object data = randomFrom(
|
Object data = randomFrom(
|
||||||
new Object[] { singletonMap("key", "foo"), singletonMap("key", "bar") },
|
new Object[] { singletonMap("key", "foo"), singletonMap("key", "bar") },
|
||||||
|
@ -142,7 +149,7 @@ public class MustacheTests extends ESTestCase {
|
||||||
// json string escaping enabled:
|
// json string escaping enabled:
|
||||||
Map<String, String> params = randomBoolean() ? Collections.emptyMap() : Collections.singletonMap(CONTENT_TYPE_PARAM, JSON_CONTENT_TYPE);
|
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());
|
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\""));
|
ExecutableScript executableScript = engine.executable(compiledScript, Collections.singletonMap("value", "a \"value\""));
|
||||||
BytesReference rawResult = (BytesReference) executableScript.run();
|
BytesReference rawResult = (BytesReference) executableScript.run();
|
||||||
String result = rawResult.toUtf8();
|
String result = rawResult.toUtf8();
|
||||||
|
@ -150,7 +157,7 @@ public class MustacheTests extends ESTestCase {
|
||||||
|
|
||||||
// json string escaping disabled:
|
// json string escaping disabled:
|
||||||
mustache = (Mustache) engine.compile(null, "{ \"field1\": \"{{value}}\"}", Collections.singletonMap(CONTENT_TYPE_PARAM, PLAIN_TEXT_CONTENT_TYPE));
|
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\""));
|
executableScript = engine.executable(compiledScript, Collections.singletonMap("value", "a \"value\""));
|
||||||
rawResult = (BytesReference) executableScript.run();
|
rawResult = (BytesReference) executableScript.run();
|
||||||
result = rawResult.toUtf8();
|
result = rawResult.toUtf8();
|
||||||
|
@ -162,7 +169,7 @@ public class MustacheTests extends ESTestCase {
|
||||||
List<String> randomList = Arrays.asList(generateRandomStringArray(10, 20, false));
|
List<String> randomList = Arrays.asList(generateRandomStringArray(10, 20, false));
|
||||||
|
|
||||||
String template = "{{data.array.size}} {{data.list.size}}";
|
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<>();
|
Map<String, Object> data = new HashMap<>();
|
||||||
data.put("array", randomArrayValues);
|
data.put("array", randomArrayValues);
|
||||||
data.put("list", randomList);
|
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());
|
String expectedString = String.format(Locale.ROOT, "%s %s", randomArrayValues.length, randomList.size());
|
||||||
assertThat(bytes.toUtf8(), equalTo(expectedString));
|
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