/* * 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.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.test.ESTestCase; import org.hamcrest.Matcher; import org.hamcrest.Matchers; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; 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; 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 { private ScriptEngineService engine = new MustacheScriptEngineService(Settings.EMPTY); public void testBasics() { String template = "GET _search {\"query\": " + "{\"boosting\": {" + "\"positive\": {\"match\": {\"body\": \"gift\"}}," + "\"negative\": {\"term\": {\"body\": {\"value\": \"solr\"}" + "}}, \"negative_boost\": {{boost_val}} } }}"; Map<String, Object> params = Collections.<String, Object>singletonMap("boost_val", "0.2"); Mustache mustache = (Mustache) engine.compile(template, Collections.<String, String>emptyMap()); CompiledScript compiledScript = new CompiledScript(INLINE, "my-name", "mustache", mustache); ExecutableScript result = engine.executable(compiledScript, params); assertEquals( "Mustache templating broken", "GET _search {\"query\": {\"boosting\": {\"positive\": {\"match\": {\"body\": \"gift\"}}," + "\"negative\": {\"term\": {\"body\": {\"value\": \"solr\"}}}, \"negative_boost\": 0.2 } }}", ((BytesReference) result.run()).toUtf8() ); } public void testArrayAccess() throws Exception { String template = "{{data.0}} {{data.1}}"; CompiledScript mustache = new CompiledScript(INLINE, "inline", "mustache", engine.compile(template, Collections.<String, String>emptyMap())); Map<String, Object> vars = new HashMap<>(); Object data = randomFrom( new String[] { "foo", "bar" }, Arrays.asList("foo", "bar")); vars.put("data", data); Object output = engine.executable(mustache, vars).run(); assertThat(output, notNullValue()); assertThat(output, instanceOf(BytesReference.class)); BytesReference bytes = (BytesReference) output; assertThat(bytes.toUtf8(), equalTo("foo bar")); // Sets can come out in any order Set<String> setData = new HashSet<>(); setData.add("foo"); setData.add("bar"); vars.put("data", setData); output = engine.executable(mustache, vars).run(); assertThat(output, notNullValue()); assertThat(output, instanceOf(BytesReference.class)); bytes = (BytesReference) output; assertThat(bytes.toUtf8(), both(containsString("foo")).and(containsString("bar"))); } public void testArrayInArrayAccess() throws Exception { String template = "{{data.0.0}} {{data.0.1}}"; CompiledScript mustache = new CompiledScript(INLINE, "inline", "mustache", engine.compile(template, Collections.<String, String>emptyMap())); Map<String, Object> vars = new HashMap<>(); Object data = randomFrom( new String[][] { new String[] { "foo", "bar" }}, Collections.singletonList(new String[] { "foo", "bar" }), singleton(new String[] { "foo", "bar" }) ); vars.put("data", data); Object output = engine.executable(mustache, vars).run(); assertThat(output, notNullValue()); assertThat(output, instanceOf(BytesReference.class)); BytesReference bytes = (BytesReference) output; assertThat(bytes.toUtf8(), equalTo("foo bar")); } public void testMapInArrayAccess() throws Exception { String template = "{{data.0.key}} {{data.1.key}}"; CompiledScript mustache = new CompiledScript(INLINE, "inline", "mustache", engine.compile(template, Collections.<String, String>emptyMap())); Map<String, Object> vars = new HashMap<>(); Object data = randomFrom( new Object[] { singletonMap("key", "foo"), singletonMap("key", "bar") }, Arrays.asList(singletonMap("key", "foo"), singletonMap("key", "bar"))); vars.put("data", data); Object output = engine.executable(mustache, vars).run(); assertThat(output, notNullValue()); assertThat(output, instanceOf(BytesReference.class)); BytesReference bytes = (BytesReference) output; assertThat(bytes.toUtf8(), equalTo("foo bar")); // HashSet iteration order isn't fixed Set<Object> setData = new HashSet<>(); setData.add(singletonMap("key", "foo")); setData.add(singletonMap("key", "bar")); vars.put("data", setData); output = engine.executable(mustache, vars).run(); assertThat(output, notNullValue()); assertThat(output, instanceOf(BytesReference.class)); bytes = (BytesReference) output; assertThat(bytes.toUtf8(), both(containsString("foo")).and(containsString("bar"))); } public void testEscaping() { // json string escaping enabled: Map<String, Object> params = randomBoolean() ? Collections.EMPTY_MAP: Collections.<String, Object>singletonMap(CONTENT_TYPE_PARAM, JSON_CONTENT_TYPE); Mustache mustache = (Mustache) engine.compile("{ \"field1\": \"{{value}}\"}", Collections.EMPTY_MAP); CompiledScript compiledScript = new CompiledScript(INLINE, "name", "mustache", mustache); ExecutableScript executableScript = engine.executable(compiledScript, Collections.<String, Object>singletonMap("value", "a \"value\"")); BytesReference rawResult = (BytesReference) executableScript.run(); String result = rawResult.toUtf8(); assertThat(result, equalTo("{ \"field1\": \"a \\\"value\\\"\"}")); // json string escaping disabled: mustache = (Mustache) engine.compile("{ \"field1\": \"{{value}}\"}", Collections.singletonMap(CONTENT_TYPE_PARAM, PLAIN_TEXT_CONTENT_TYPE)); compiledScript = new CompiledScript(INLINE, "name", "mustache", mustache); executableScript = engine.executable(compiledScript, Collections.<String, Object>singletonMap("value", "a \"value\"")); rawResult = (BytesReference) executableScript.run(); result = rawResult.toUtf8(); assertThat(result, equalTo("{ \"field1\": \"a \"value\"\"}")); } public void testSizeAccessForCollectionsAndArrays() throws Exception { String[] randomArrayValues = generateRandomStringArray(10, 20, false); List<String> randomList = Arrays.asList(generateRandomStringArray(10, 20, false)); String template = "{{data.array.size}} {{data.list.size}}"; CompiledScript mustache = new CompiledScript(INLINE, "inline", "mustache", engine.compile(template, Collections.<String, String>emptyMap())); Map<String, Object> data = new HashMap<>(); data.put("array", randomArrayValues); data.put("list", randomList); Map<String, Object> vars = new HashMap<>(); vars.put("data", data); Object output = engine.executable(mustache, vars).run(); assertThat(output, notNullValue()); assertThat(output, instanceOf(BytesReference.class)); BytesReference bytes = (BytesReference) output; 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.<String, Object>singletonMap("ctx", "value"), Matchers.<Object>equalTo("value")); assertScript(template, Collections.<String, Object>singletonMap("ctx", ""), Matchers.<Object>equalTo("")); assertScript(template, Collections.<String, Object>singletonMap("ctx", true), Matchers.<Object>equalTo("true")); assertScript(template, Collections.<String, Object>singletonMap("ctx", 42), Matchers.<Object>equalTo("42")); assertScript(template, Collections.<String, Object>singletonMap("ctx", 42L), Matchers.<Object>equalTo("42")); assertScript(template, Collections.<String, Object>singletonMap("ctx", 42.5f), Matchers.<Object>equalTo("42.5")); assertScript(template, Collections.singletonMap("ctx", null), Matchers.<Object>equalTo("")); template = "{{#toJson}}.{{/toJson}}"; assertScript(template, Collections.<String, Object>singletonMap("ctx", "value"), Matchers.<Object>equalTo("{\"ctx\":\"value\"}")); assertScript(template, Collections.<String, Object>singletonMap("ctx", ""), Matchers.<Object>equalTo("{\"ctx\":\"\"}")); assertScript(template, Collections.<String, Object>singletonMap("ctx", true), Matchers.<Object>equalTo("{\"ctx\":true}")); assertScript(template, Collections.<String, Object>singletonMap("ctx", 42), Matchers.<Object>equalTo("{\"ctx\":42}")); assertScript(template, Collections.<String, Object>singletonMap("ctx", 42L), Matchers.<Object>equalTo("{\"ctx\":42}")); assertScript(template, Collections.<String, Object>singletonMap("ctx", 42.5f), Matchers.<Object>equalTo("{\"ctx\":42.5}")); assertScript(template, Collections.singletonMap("ctx", null), Matchers.<Object>equalTo("{\"ctx\":null}")); } public void testSimpleMapToJSON() throws Exception { Map<String, Object> human0 = new LinkedHashMap<>(); human0.put("name", "John Smith"); human0.put("age", 42); human0.put("height", 1.84); Map<String, Object> ctx = Collections.<String, Object>singletonMap("ctx", human0); assertScript("{{#toJson}}.{{/toJson}}", ctx, Matchers.<Object>equalTo("{\"ctx\":{\"name\":\"John Smith\",\"age\":42,\"height\":1.84}}")); assertScript("{{#toJson}}ctx{{/toJson}}", ctx, Matchers.<Object>equalTo("{\"name\":\"John Smith\",\"age\":42,\"height\":1.84}")); assertScript("{{#toJson}}ctx.name{{/toJson}}", ctx, Matchers.<Object>equalTo("John Smith")); } public void testMultipleMapsToJSON() throws Exception { Map<String, Object> human0 = new LinkedHashMap<>(); human0.put("name", "John Smith"); human0.put("age", 42); human0.put("height", 1.84); Map<String, Object> human1 = new LinkedHashMap<>(); human1.put("name", "Dave Smith"); human1.put("age", 27); human1.put("height", 1.71); Map<String, Object> humans = new LinkedHashMap<>(); humans.put("first", human0); humans.put("second", human1); Map<String, Object> ctx = Collections.<String, Object>singletonMap("ctx", humans); assertScript("{{#toJson}}.{{/toJson}}", ctx, Matchers.<Object>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, Matchers.<Object>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, Matchers.<Object>equalTo("{\"name\":\"John Smith\",\"age\":42,\"height\":1.84}")); assertScript("{{#toJson}}ctx.second{{/toJson}}", ctx, Matchers.<Object>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.<String, Object>singletonMap("array", array); assertScript("{{#toJson}}.{{/toJson}}", ctx, Matchers.<Object>equalTo("{\"array\":[\"one\",\"two\",\"three\"]}")); assertScript("{{#toJson}}array{{/toJson}}", ctx, Matchers.<Object>equalTo("[\"one\",\"two\",\"three\"]")); assertScript("{{#toJson}}array.0{{/toJson}}", ctx, Matchers.<Object>equalTo("one")); assertScript("{{#toJson}}array.1{{/toJson}}", ctx, Matchers.<Object>equalTo("two")); assertScript("{{#toJson}}array.2{{/toJson}}", ctx, Matchers.<Object>equalTo("three")); assertScript("{{#toJson}}array.size{{/toJson}}", ctx, Matchers.<Object>equalTo("3")); } public void testSimpleListToJSON() throws Exception { List<String> list = Arrays.asList("one", "two", "three"); Map<String, Object> ctx = Collections.<String, Object>singletonMap("ctx", list); assertScript("{{#toJson}}.{{/toJson}}", ctx, Matchers.<Object>equalTo("{\"ctx\":[\"one\",\"two\",\"three\"]}")); assertScript("{{#toJson}}ctx{{/toJson}}", ctx, Matchers.<Object>equalTo("[\"one\",\"two\",\"three\"]")); assertScript("{{#toJson}}ctx.0{{/toJson}}", ctx, Matchers.<Object>equalTo("one")); assertScript("{{#toJson}}ctx.1{{/toJson}}", ctx, Matchers.<Object>equalTo("two")); assertScript("{{#toJson}}ctx.2{{/toJson}}", ctx, Matchers.<Object>equalTo("three")); assertScript("{{#toJson}}ctx.size{{/toJson}}", ctx, Matchers.<Object>equalTo("3")); } public void testsUnsupportedTagsToJson() { try { compile("{{#toJson}}{{foo}}{{bar}}{{/toJson}}"); fail("Expected MustacheException"); } catch (MustacheException e) { assertThat(e.getMessage(), containsString("Mustache function [toJson] must contain one and only one identifier")); } try { compile("{{#toJson}}{{/toJson}}"); fail("Expected MustacheException"); } catch (MustacheException e) { 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.<String, Object>singletonMap("ctx", XContentHelper.convertToMap(builder.bytes(), true).v2()); assertScript("{{#ctx.bulks}}{{#toJson}}.{{/toJson}}{{/ctx.bulks}}", ctx, Matchers.<Object>equalTo("{\"index\":\"index-1\",\"type\":\"type-1\",\"id\":1}{\"index\":\"index-2\",\"type\":\"type-2\"," + "\"id\":2}")); assertScript("{{#ctx.bulks}}<{{#toJson}}id{{/toJson}}>{{/ctx.bulks}}", ctx, Matchers.<Object>equalTo("<1><2>")); } public void testSimpleArrayJoin() throws Exception { String template = "{{#join}}array{{/join}}"; assertScript(template, Collections.<String, Object>singletonMap("array", new String[]{"one", "two", "three"}), Matchers.<Object>equalTo("one,two,three")); assertScript(template, Collections.<String, Object>singletonMap("array", new int[]{1, 2, 3}), Matchers.<Object>equalTo("1,2,3")); assertScript(template, Collections.<String, Object>singletonMap("array", new long[]{1L, 2L, 3L}), Matchers.<Object>equalTo("1,2,3")); assertScript(template, Collections.<String, Object>singletonMap("array", new double[]{1.5, 2.5, 3.5}), Matchers.<Object>equalTo("1.5,2.5,3.5")); assertScript(template, Collections.<String, Object>singletonMap("array", new boolean[]{true, false, true}), Matchers.<Object>equalTo("true,false,true")); assertScript(template, Collections.<String, Object>singletonMap("array", new boolean[]{true, false, true}), Matchers.<Object>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.<String, Object>singletonMap("ctx", XContentHelper.convertToMap(builder.bytes(), false).v2()); assertScript("{{#join}}ctx.people.0.emails{{/join}}", ctx, Matchers.<Object>equalTo("john@smith.com,john.smith@email.com,jsmith@email.com")); assertScript("{{#join}}ctx.people.1.emails{{/join}}", ctx, Matchers.<Object>equalTo("john@doe.com,john.doe@email.com,jdoe@email.com")); assertScript("{{#ctx.people}}to: {{#join}}emails{{/join}};{{/ctx.people}}", ctx, Matchers.<Object>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.<String, Object>singletonMap("terms", Arrays.asList(singletonMap("term", "foo"), singletonMap("term", "bar"))); assertScript("{{#join}}{{#toJson}}terms{{/toJson}}{{/join}}", params, Matchers.<Object>equalTo("[{\"term\":\"foo\"},{\"term\":\"bar\"}]")); } public void testsUnsupportedTagsJoin() { try { compile("{{#join}}{{/join}}"); } catch (MustacheException e) { assertThat(e.getMessage(), containsString("Mustache function [join] must contain one and only one identifier")); } try { compile("{{#join delimiter='a'}}{{/join delimiter='b'}}"); } catch (MustacheException e) { assertThat(e.getMessage(), containsString("Mismatched start/end tags")); } } public void testJoinWithCustomDelimiter() { Map<String, Object> params = Collections.<String, Object>singletonMap("params", Arrays.asList(1, 2, 3, 4)); assertScript("{{#join delimiter=''}}params{{/join delimiter=''}}", params, Matchers.<Object>equalTo("1234")); assertScript("{{#join delimiter=','}}params{{/join delimiter=','}}", params, Matchers.<Object>equalTo("1,2,3,4")); assertScript("{{#join delimiter='/'}}params{{/join delimiter='/'}}", params, Matchers.<Object>equalTo("1/2/3/4")); assertScript("{{#join delimiter=' and '}}params{{/join delimiter=' and '}}", params, Matchers.<Object>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(script, Collections.<String, String>emptyMap()); } }