From d59ccf0b9a9ff5487d05c89ae07df3afc889fe46 Mon Sep 17 00:00:00 2001 From: Julie Tibshirani Date: Tue, 1 May 2018 17:37:33 -0700 Subject: [PATCH 1/7] Move request parsing logic into SearchTemplateRequest. --- .../RestMultiSearchTemplateAction.java | 2 +- .../RestRenderSearchTemplateAction.java | 2 +- .../mustache/RestSearchTemplateAction.java | 33 +--------------- .../mustache/SearchTemplateRequest.java | 38 +++++++++++++++++++ .../script/mustache/SearchTemplateIT.java | 6 +-- .../mustache/SearchTemplateRequestTests.java | 14 +++---- 6 files changed, 51 insertions(+), 44 deletions(-) diff --git a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestMultiSearchTemplateAction.java b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestMultiSearchTemplateAction.java index fd797c4340a8f..9969e6b38e54a 100644 --- a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestMultiSearchTemplateAction.java +++ b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestMultiSearchTemplateAction.java @@ -77,7 +77,7 @@ public static MultiSearchTemplateRequest parseRequest(RestRequest restRequest, b RestMultiSearchAction.parseMultiLineRequest(restRequest, multiRequest.indicesOptions(), allowExplicitIndex, (searchRequest, bytes) -> { - SearchTemplateRequest searchTemplateRequest = RestSearchTemplateAction.parse(bytes); + SearchTemplateRequest searchTemplateRequest = SearchTemplateRequest.fromXContent(bytes); if (searchTemplateRequest.getScript() != null) { searchTemplateRequest.setRequest(searchRequest); multiRequest.add(searchTemplateRequest); diff --git a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestRenderSearchTemplateAction.java b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestRenderSearchTemplateAction.java index d8c67839cb80f..75acc09424359 100644 --- a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestRenderSearchTemplateAction.java +++ b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestRenderSearchTemplateAction.java @@ -52,7 +52,7 @@ public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client // Creates the render template request SearchTemplateRequest renderRequest; try (XContentParser parser = request.contentOrSourceParamParser()) { - renderRequest = RestSearchTemplateAction.parse(parser); + renderRequest = SearchTemplateRequest.fromXContent(parser); } renderRequest.setSimulate(true); diff --git a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestSearchTemplateAction.java b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestSearchTemplateAction.java index 7ab9aa6003334..f42afcc19b80f 100644 --- a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestSearchTemplateAction.java +++ b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestSearchTemplateAction.java @@ -47,33 +47,6 @@ public class RestSearchTemplateAction extends BaseRestHandler { private static final Set RESPONSE_PARAMS = Collections.singleton(RestSearchAction.TYPED_KEYS_PARAM); - private static final ObjectParser PARSER; - static { - PARSER = new ObjectParser<>("search_template"); - PARSER.declareField((parser, request, s) -> - request.setScriptParams(parser.map()) - , new ParseField("params"), ObjectParser.ValueType.OBJECT); - PARSER.declareString((request, s) -> { - request.setScriptType(ScriptType.STORED); - request.setScript(s); - }, new ParseField("id")); - PARSER.declareBoolean(SearchTemplateRequest::setExplain, new ParseField("explain")); - PARSER.declareBoolean(SearchTemplateRequest::setProfile, new ParseField("profile")); - PARSER.declareField((parser, request, value) -> { - request.setScriptType(ScriptType.INLINE); - if (parser.currentToken() == XContentParser.Token.START_OBJECT) { - //convert the template to json which is the only supported XContentType (see CustomMustacheFactory#createEncoder) - try (XContentBuilder builder = XContentFactory.jsonBuilder()) { - request.setScript(Strings.toString(builder.copyCurrentStructure(parser))); - } catch (IOException e) { - throw new ParsingException(parser.getTokenLocation(), "Could not parse inline template", e); - } - } else { - request.setScript(parser.text()); - } - }, new ParseField("source", "inline", "template"), ObjectParser.ValueType.OBJECT_OR_STRING); - } - public RestSearchTemplateAction(Settings settings, RestController controller) { super(settings); @@ -99,17 +72,13 @@ public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client // Creates the search template request SearchTemplateRequest searchTemplateRequest; try (XContentParser parser = request.contentOrSourceParamParser()) { - searchTemplateRequest = PARSER.parse(parser, new SearchTemplateRequest(), null); + searchTemplateRequest = SearchTemplateRequest.fromXContent(parser); } searchTemplateRequest.setRequest(searchRequest); return channel -> client.execute(SearchTemplateAction.INSTANCE, searchTemplateRequest, new RestStatusToXContentListener<>(channel)); } - public static SearchTemplateRequest parse(XContentParser parser) throws IOException { - return PARSER.parse(parser, new SearchTemplateRequest(), null); - } - @Override protected Set responseParams() { return RESPONSE_PARAMS; diff --git a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/SearchTemplateRequest.java b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/SearchTemplateRequest.java index b0186b7b0e3cf..d7182151e77c1 100644 --- a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/SearchTemplateRequest.java +++ b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/SearchTemplateRequest.java @@ -23,8 +23,15 @@ import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.CompositeIndicesRequest; import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.ParsingException; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.script.ScriptType; import java.io.IOException; @@ -134,6 +141,37 @@ public ActionRequestValidationException validate() { return validationException; } + private static final ObjectParser PARSER; + static { + PARSER = new ObjectParser<>("search_template"); + PARSER.declareField((parser, request, s) -> + request.setScriptParams(parser.map()) + , new ParseField("params"), ObjectParser.ValueType.OBJECT); + PARSER.declareString((request, s) -> { + request.setScriptType(ScriptType.STORED); + request.setScript(s); + }, new ParseField("id")); + PARSER.declareBoolean(SearchTemplateRequest::setExplain, new ParseField("explain")); + PARSER.declareBoolean(SearchTemplateRequest::setProfile, new ParseField("profile")); + PARSER.declareField((parser, request, value) -> { + request.setScriptType(ScriptType.INLINE); + if (parser.currentToken() == XContentParser.Token.START_OBJECT) { + //convert the template to json which is the only supported XContentType (see CustomMustacheFactory#createEncoder) + try (XContentBuilder builder = XContentFactory.jsonBuilder()) { + request.setScript(Strings.toString(builder.copyCurrentStructure(parser))); + } catch (IOException e) { + throw new ParsingException(parser.getTokenLocation(), "Could not parse inline template", e); + } + } else { + request.setScript(parser.text()); + } + }, new ParseField("source", "inline", "template"), ObjectParser.ValueType.OBJECT_OR_STRING); + } + + public static SearchTemplateRequest fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, new SearchTemplateRequest(), null); + } + @Override public void readFrom(StreamInput in) throws IOException { super.readFrom(in); diff --git a/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateIT.java b/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateIT.java index 1529b655a5042..fe2fedf62b559 100644 --- a/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateIT.java +++ b/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateIT.java @@ -101,7 +101,7 @@ public void testTemplateQueryAsEscapedString() throws Exception { + " \"size\": 1" + " }" + "}"; - SearchTemplateRequest request = RestSearchTemplateAction.parse(createParser(JsonXContent.jsonXContent, query)); + SearchTemplateRequest request = SearchTemplateRequest.fromXContent(createParser(JsonXContent.jsonXContent, query)); request.setRequest(searchRequest); SearchTemplateResponse searchResponse = client().execute(SearchTemplateAction.INSTANCE, request).get(); assertThat(searchResponse.getResponse().getHits().getHits().length, equalTo(1)); @@ -122,7 +122,7 @@ public void testTemplateQueryAsEscapedStringStartingWithConditionalClause() thro + " \"use_size\": true" + " }" + "}"; - SearchTemplateRequest request = RestSearchTemplateAction.parse(createParser(JsonXContent.jsonXContent, templateString)); + SearchTemplateRequest request = SearchTemplateRequest.fromXContent(createParser(JsonXContent.jsonXContent, templateString)); request.setRequest(searchRequest); SearchTemplateResponse searchResponse = client().execute(SearchTemplateAction.INSTANCE, request).get(); assertThat(searchResponse.getResponse().getHits().getHits().length, equalTo(1)); @@ -143,7 +143,7 @@ public void testTemplateQueryAsEscapedStringWithConditionalClauseAtEnd() throws + " \"use_size\": true" + " }" + "}"; - SearchTemplateRequest request = RestSearchTemplateAction.parse(createParser(JsonXContent.jsonXContent, templateString)); + SearchTemplateRequest request = SearchTemplateRequest.fromXContent(createParser(JsonXContent.jsonXContent, templateString)); request.setRequest(searchRequest); SearchTemplateResponse searchResponse = client().execute(SearchTemplateAction.INSTANCE, request).get(); assertThat(searchResponse.getResponse().getHits().getHits().length, equalTo(1)); diff --git a/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateRequestTests.java b/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateRequestTests.java index 9cdca70f0e1a6..4041a7e3e709b 100644 --- a/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateRequestTests.java +++ b/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateRequestTests.java @@ -51,7 +51,7 @@ public void testParseInlineTemplate() throws Exception { " }" + "}"; - SearchTemplateRequest request = RestSearchTemplateAction.parse(newParser(source)); + SearchTemplateRequest request = SearchTemplateRequest.fromXContent(newParser(source)); assertThat(request.getScript(), equalTo("{\"query\":{\"terms\":{\"status\":[\"{{#status}}\",\"{{.}}\",\"{{/status}}\"]}}}")); assertThat(request.getScriptType(), equalTo(ScriptType.INLINE)); assertThat(request.getScriptParams(), nullValue()); @@ -70,7 +70,7 @@ public void testParseInlineTemplateWithParams() throws Exception { " }" + "}"; - SearchTemplateRequest request = RestSearchTemplateAction.parse(newParser(source)); + SearchTemplateRequest request = SearchTemplateRequest.fromXContent(newParser(source)); assertThat(request.getScript(), equalTo("{\"query\":{\"match\":{\"{{my_field}}\":\"{{my_value}}\"}},\"size\":\"{{my_size}}\"}")); assertThat(request.getScriptType(), equalTo(ScriptType.INLINE)); assertThat(request.getScriptParams().size(), equalTo(3)); @@ -82,7 +82,7 @@ public void testParseInlineTemplateWithParams() throws Exception { public void testParseInlineTemplateAsString() throws Exception { String source = "{'source' : '{\\\"query\\\":{\\\"bool\\\":{\\\"must\\\":{\\\"match\\\":{\\\"foo\\\":\\\"{{text}}\\\"}}}}}'}"; - SearchTemplateRequest request = RestSearchTemplateAction.parse(newParser(source)); + SearchTemplateRequest request = SearchTemplateRequest.fromXContent(newParser(source)); assertThat(request.getScript(), equalTo("{\"query\":{\"bool\":{\"must\":{\"match\":{\"foo\":\"{{text}}\"}}}}}")); assertThat(request.getScriptType(), equalTo(ScriptType.INLINE)); assertThat(request.getScriptParams(), nullValue()); @@ -93,7 +93,7 @@ public void testParseInlineTemplateAsStringWithParams() throws Exception { String source = "{'source' : '{\\\"query\\\":{\\\"match\\\":{\\\"{{field}}\\\":\\\"{{value}}\\\"}}}', " + "'params': {'status': ['pending', 'published']}}"; - SearchTemplateRequest request = RestSearchTemplateAction.parse(newParser(source)); + SearchTemplateRequest request = SearchTemplateRequest.fromXContent(newParser(source)); assertThat(request.getScript(), equalTo("{\"query\":{\"match\":{\"{{field}}\":\"{{value}}\"}}}")); assertThat(request.getScriptType(), equalTo(ScriptType.INLINE)); assertThat(request.getScriptParams().size(), equalTo(1)); @@ -104,7 +104,7 @@ public void testParseInlineTemplateAsStringWithParams() throws Exception { public void testParseStoredTemplate() throws Exception { String source = "{'id' : 'storedTemplate'}"; - SearchTemplateRequest request = RestSearchTemplateAction.parse(newParser(source)); + SearchTemplateRequest request = SearchTemplateRequest.fromXContent(newParser(source)); assertThat(request.getScript(), equalTo("storedTemplate")); assertThat(request.getScriptType(), equalTo(ScriptType.STORED)); assertThat(request.getScriptParams(), nullValue()); @@ -113,7 +113,7 @@ public void testParseStoredTemplate() throws Exception { public void testParseStoredTemplateWithParams() throws Exception { String source = "{'id' : 'another_template', 'params' : {'bar': 'foo'}}"; - SearchTemplateRequest request = RestSearchTemplateAction.parse(newParser(source)); + SearchTemplateRequest request = SearchTemplateRequest.fromXContent(newParser(source)); assertThat(request.getScript(), equalTo("another_template")); assertThat(request.getScriptType(), equalTo(ScriptType.STORED)); assertThat(request.getScriptParams().size(), equalTo(1)); @@ -122,7 +122,7 @@ public void testParseStoredTemplateWithParams() throws Exception { public void testParseWrongTemplate() { // Unclosed template id - expectThrows(XContentParseException.class, () -> RestSearchTemplateAction.parse(newParser("{'id' : 'another_temp }"))); + expectThrows(XContentParseException.class, () -> SearchTemplateRequest.fromXContent(newParser("{'id' : 'another_temp }"))); } /** From 379c400b60f100a9b1b1f9697feb2d9751610249 Mon Sep 17 00:00:00 2001 From: Julie Tibshirani Date: Wed, 2 May 2018 13:03:39 -0700 Subject: [PATCH 2/7] Allow for SearchTemplateRequest to be serialized to XContent. --- .../mustache/SearchTemplateRequest.java | 57 ++++- .../mustache/SearchTemplateRequestTests.java | 230 +++++++++++++----- .../search/RandomSearchRequestGenerator.java | 2 +- 3 files changed, 224 insertions(+), 65 deletions(-) diff --git a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/SearchTemplateRequest.java b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/SearchTemplateRequest.java index d7182151e77c1..5e76038076a6e 100644 --- a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/SearchTemplateRequest.java +++ b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/SearchTemplateRequest.java @@ -29,6 +29,7 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentParser; @@ -36,13 +37,14 @@ import java.io.IOException; import java.util.Map; +import java.util.Objects; import static org.elasticsearch.action.ValidateActions.addValidationError; /** * A request to execute a search based on a search template. */ -public class SearchTemplateRequest extends ActionRequest implements CompositeIndicesRequest { +public class SearchTemplateRequest extends ActionRequest implements CompositeIndicesRequest, ToXContentObject { private SearchRequest request; private boolean simulate = false; @@ -67,6 +69,24 @@ public SearchRequest getRequest() { return request; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SearchTemplateRequest request1 = (SearchTemplateRequest) o; + return simulate == request1.simulate && + explain == request1.explain && + profile == request1.profile && + Objects.equals(request, request1.request) && + scriptType == request1.scriptType && + Objects.equals(script, request1.script) && + Objects.equals(scriptParams, request1.scriptParams); + } + + @Override + public int hashCode() { + return Objects.hash(request, simulate, explain, profile, scriptType, script, scriptParams); + } public boolean isSimulate() { return simulate; @@ -141,18 +161,25 @@ public ActionRequestValidationException validate() { return validationException; } + private static ParseField ID_FIELD = new ParseField("id"); + private static ParseField SOURCE_FIELD = new ParseField("source", "inline", "template"); + + private static ParseField PARAMS_FIELD = new ParseField("params"); + private static ParseField EXPLAIN_FIELD = new ParseField("explain"); + private static ParseField PROFILE_FIELD = new ParseField("profile"); + private static final ObjectParser PARSER; static { PARSER = new ObjectParser<>("search_template"); PARSER.declareField((parser, request, s) -> request.setScriptParams(parser.map()) - , new ParseField("params"), ObjectParser.ValueType.OBJECT); + , PARAMS_FIELD, ObjectParser.ValueType.OBJECT); PARSER.declareString((request, s) -> { request.setScriptType(ScriptType.STORED); request.setScript(s); - }, new ParseField("id")); - PARSER.declareBoolean(SearchTemplateRequest::setExplain, new ParseField("explain")); - PARSER.declareBoolean(SearchTemplateRequest::setProfile, new ParseField("profile")); + }, ID_FIELD); + PARSER.declareBoolean(SearchTemplateRequest::setExplain, EXPLAIN_FIELD); + PARSER.declareBoolean(SearchTemplateRequest::setProfile, PROFILE_FIELD); PARSER.declareField((parser, request, value) -> { request.setScriptType(ScriptType.INLINE); if (parser.currentToken() == XContentParser.Token.START_OBJECT) { @@ -165,13 +192,31 @@ public ActionRequestValidationException validate() { } else { request.setScript(parser.text()); } - }, new ParseField("source", "inline", "template"), ObjectParser.ValueType.OBJECT_OR_STRING); + }, SOURCE_FIELD, ObjectParser.ValueType.OBJECT_OR_STRING); } public static SearchTemplateRequest fromXContent(XContentParser parser) throws IOException { return PARSER.parse(parser, new SearchTemplateRequest(), null); } + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + + if (scriptType == ScriptType.STORED) { + builder.field(ID_FIELD.getPreferredName(), script); + } else if (scriptType == ScriptType.INLINE) { + builder.field(SOURCE_FIELD.getPreferredName(), script); + } else { + throw new IllegalArgumentException("Unrecognized script type [" + scriptType + "]."); + } + + return builder.field(PARAMS_FIELD.getPreferredName(), scriptParams) + .field(EXPLAIN_FIELD.getPreferredName(), explain) + .field(PROFILE_FIELD.getPreferredName(), profile) + .endObject(); + } + @Override public void readFrom(StreamInput in) throws IOException { super.readFrom(in); diff --git a/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateRequestTests.java b/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateRequestTests.java index 4041a7e3e709b..4d52815909ab1 100644 --- a/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateRequestTests.java +++ b/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateRequestTests.java @@ -19,24 +19,179 @@ package org.elasticsearch.script.mustache; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentParseException; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.script.ScriptType; -import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.search.RandomSearchRequestGenerator; +import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.test.AbstractStreamableTestCase; +import org.elasticsearch.test.AbstractXContentTestCase; import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasEntry; -import static org.hamcrest.Matchers.hasItems; -import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.nullValue; -public class SearchTemplateRequestTests extends ESTestCase { +public class SearchTemplateRequestTests extends AbstractStreamableTestCase { - public void testParseInlineTemplate() throws Exception { + @Override + protected SearchTemplateRequest createBlankInstance() { + return new SearchTemplateRequest(); + } + + @Override + protected SearchTemplateRequest createTestInstance() { + SearchTemplateRequest request = new SearchTemplateRequest(); + request.setScriptType(randomFrom(ScriptType.values())); + request.setScript(randomAlphaOfLength(50)); + + Map scriptParams = new HashMap<>(); + for (int i = 0; i < randomInt(10); i++) { + scriptParams.put(randomAlphaOfLength(5), randomAlphaOfLength(10)); + } + request.setScriptParams(scriptParams); + + request.setExplain(randomBoolean()); + request.setProfile(randomBoolean()); + request.setSimulate(randomBoolean()); + + request.setRequest(RandomSearchRequestGenerator.randomSearchRequest( + SearchSourceBuilder::searchSource)); + return request; + } + + @Override + protected SearchTemplateRequest mutateInstance(SearchTemplateRequest instance) throws IOException { + List> mutators = new ArrayList<>(); + + mutators.add(request -> request.setScriptType( + randomValueOtherThan(request.getScriptType(), () -> randomFrom(ScriptType.values())))); + mutators.add(request -> request.setScript( + randomValueOtherThan(request.getScript(), () -> randomAlphaOfLength(50)))); + + mutators.add(request -> { + Map mutatedScriptParams = new HashMap<>(request.getScriptParams()); + String newField = randomValueOtherThanMany(mutatedScriptParams::containsKey, () -> randomAlphaOfLength(5)); + mutatedScriptParams.put(newField, randomAlphaOfLength(10)); + request.setScriptParams(mutatedScriptParams); + }); + + mutators.add(request -> request.setProfile(!request.isProfile())); + mutators.add(request -> request.setExplain(!request.isExplain())); + mutators.add(request -> request.setSimulate(!request.isSimulate())); + + mutators.add(request -> request.setRequest( + RandomSearchRequestGenerator.randomSearchRequest(SearchSourceBuilder::searchSource))); + + SearchTemplateRequest mutatedInstance = copyInstance(instance); + Consumer mutator = randomFrom(mutators); + mutator.accept(mutatedInstance); + return mutatedInstance; + } + + public void testToXContentWithInlineTemplate() throws IOException { + SearchTemplateRequest request = new SearchTemplateRequest(); + + request.setScriptType(ScriptType.INLINE); + request.setScript("{\"query\": { \"match\" : { \"{{my_field}}\" : \"{{my_value}}\" } } }"); + request.setProfile(true); + + Map scriptParams = new HashMap<>(); + scriptParams.put("my_field", "foo"); + scriptParams.put("my_value", "bar"); + request.setScriptParams(scriptParams); + + XContentType contentType = randomFrom(XContentType.values()); + XContentBuilder expectedRequest = XContentFactory.contentBuilder(contentType) + .startObject() + .field("source", "{\"query\": { \"match\" : { \"{{my_field}}\" : \"{{my_value}}\" } } }") + .startObject("params") + .field("my_field", "foo") + .field("my_value", "bar") + .endObject() + .field("explain", false) + .field("profile", true) + .endObject(); + + XContentBuilder actualRequest = XContentFactory.contentBuilder(contentType); + request.toXContent(actualRequest, ToXContent.EMPTY_PARAMS); + + assertToXContentEquivalent(BytesReference.bytes(expectedRequest), + BytesReference.bytes(actualRequest), + contentType); + } + + public void testToXContentWithStoredTemplate() throws IOException { + SearchTemplateRequest request = new SearchTemplateRequest(); + + request.setScriptType(ScriptType.STORED); + request.setScript("match_template"); + request.setExplain(true); + + Map params = new HashMap<>(); + params.put("my_field", "foo"); + params.put("my_value", "bar"); + request.setScriptParams(params); + + XContentType contentType = randomFrom(XContentType.values()); + XContentBuilder expectedRequest = XContentFactory.contentBuilder(contentType) + .startObject() + .field("id", "match_template") + .startObject("params") + .field("my_field", "foo") + .field("my_value", "bar") + .endObject() + .field("explain", true) + .field("profile", false) + .endObject(); + + XContentBuilder actualRequest = XContentFactory.contentBuilder(contentType); + request.toXContent(actualRequest, ToXContent.EMPTY_PARAMS); + + assertToXContentEquivalent( + BytesReference.bytes(expectedRequest), + BytesReference.bytes(actualRequest), + contentType); + } + + /** + * Note that for xContent parsing, we omit two parts of the request: + * - The 'simulate' option is always held constant, since this parameter is not included + * in the request's xContent (it's instead used to determine the request endpoint). + * - We omit the random SearchRequest, since this component only affects the request + * parameters and also isn't captured in the request's xContent. + */ + public void testFromXContent() throws IOException { + AbstractXContentTestCase.testFromXContent(NUMBER_OF_TEST_RUNS, + this::createTestInstanceForXContent, + false, + new String[]{}, field -> false, + this::createParser, + SearchTemplateRequest::fromXContent, + this::assertEqualInstances, true); + } + + private SearchTemplateRequest createTestInstanceForXContent() { + SearchTemplateRequest request = createTestInstance(); + request.setSimulate(false); + request.setRequest(null); + return request; + } + + public void testFromXContentWithEmbeddedTemplate() throws Exception { String source = "{" + " 'source' : {\n" + " 'query': {\n" + @@ -57,18 +212,18 @@ public void testParseInlineTemplate() throws Exception { assertThat(request.getScriptParams(), nullValue()); } - public void testParseInlineTemplateWithParams() throws Exception { + public void testFromXContentWithEmbeddedTemplateAndParams() throws Exception { String source = "{" + - " 'source' : {" + - " 'query': { 'match' : { '{{my_field}}' : '{{my_value}}' } }," + - " 'size' : '{{my_size}}'" + - " }," + - " 'params' : {" + - " 'my_field' : 'foo'," + - " 'my_value' : 'bar'," + - " 'my_size' : 5" + - " }" + - "}"; + " 'source' : {" + + " 'query': { 'match' : { '{{my_field}}' : '{{my_value}}' } }," + + " 'size' : '{{my_size}}'" + + " }," + + " 'params' : {" + + " 'my_field' : 'foo'," + + " 'my_value' : 'bar'," + + " 'my_size' : 5" + + " }" + + "}"; SearchTemplateRequest request = SearchTemplateRequest.fromXContent(newParser(source)); assertThat(request.getScript(), equalTo("{\"query\":{\"match\":{\"{{my_field}}\":\"{{my_value}}\"}},\"size\":\"{{my_size}}\"}")); @@ -79,48 +234,7 @@ public void testParseInlineTemplateWithParams() throws Exception { assertThat(request.getScriptParams(), hasEntry("my_size", 5)); } - public void testParseInlineTemplateAsString() throws Exception { - String source = "{'source' : '{\\\"query\\\":{\\\"bool\\\":{\\\"must\\\":{\\\"match\\\":{\\\"foo\\\":\\\"{{text}}\\\"}}}}}'}"; - - SearchTemplateRequest request = SearchTemplateRequest.fromXContent(newParser(source)); - assertThat(request.getScript(), equalTo("{\"query\":{\"bool\":{\"must\":{\"match\":{\"foo\":\"{{text}}\"}}}}}")); - assertThat(request.getScriptType(), equalTo(ScriptType.INLINE)); - assertThat(request.getScriptParams(), nullValue()); - } - - @SuppressWarnings("unchecked") - public void testParseInlineTemplateAsStringWithParams() throws Exception { - String source = "{'source' : '{\\\"query\\\":{\\\"match\\\":{\\\"{{field}}\\\":\\\"{{value}}\\\"}}}', " + - "'params': {'status': ['pending', 'published']}}"; - - SearchTemplateRequest request = SearchTemplateRequest.fromXContent(newParser(source)); - assertThat(request.getScript(), equalTo("{\"query\":{\"match\":{\"{{field}}\":\"{{value}}\"}}}")); - assertThat(request.getScriptType(), equalTo(ScriptType.INLINE)); - assertThat(request.getScriptParams().size(), equalTo(1)); - assertThat(request.getScriptParams(), hasKey("status")); - assertThat((List) request.getScriptParams().get("status"), hasItems("pending", "published")); - } - - public void testParseStoredTemplate() throws Exception { - String source = "{'id' : 'storedTemplate'}"; - - SearchTemplateRequest request = SearchTemplateRequest.fromXContent(newParser(source)); - assertThat(request.getScript(), equalTo("storedTemplate")); - assertThat(request.getScriptType(), equalTo(ScriptType.STORED)); - assertThat(request.getScriptParams(), nullValue()); - } - - public void testParseStoredTemplateWithParams() throws Exception { - String source = "{'id' : 'another_template', 'params' : {'bar': 'foo'}}"; - - SearchTemplateRequest request = SearchTemplateRequest.fromXContent(newParser(source)); - assertThat(request.getScript(), equalTo("another_template")); - assertThat(request.getScriptType(), equalTo(ScriptType.STORED)); - assertThat(request.getScriptParams().size(), equalTo(1)); - assertThat(request.getScriptParams(), hasEntry("bar", "foo")); - } - - public void testParseWrongTemplate() { + public void testFromXContentWithMalformedRequest() { // Unclosed template id expectThrows(XContentParseException.class, () -> SearchTemplateRequest.fromXContent(newParser("{'id' : 'another_temp }"))); } diff --git a/test/framework/src/main/java/org/elasticsearch/search/RandomSearchRequestGenerator.java b/test/framework/src/main/java/org/elasticsearch/search/RandomSearchRequestGenerator.java index fa851e9c6d802..d534af5789448 100644 --- a/test/framework/src/main/java/org/elasticsearch/search/RandomSearchRequestGenerator.java +++ b/test/framework/src/main/java/org/elasticsearch/search/RandomSearchRequestGenerator.java @@ -82,7 +82,7 @@ private RandomSearchRequestGenerator() {} * @param randomSearchSourceBuilder builds a random {@link SearchSourceBuilder}. You can use * {@link #randomSearchSourceBuilder(Supplier, Supplier, Supplier, Supplier, Supplier)}. */ - public static SearchRequest randomSearchRequest(Supplier randomSearchSourceBuilder) throws IOException { + public static SearchRequest randomSearchRequest(Supplier randomSearchSourceBuilder) { SearchRequest searchRequest = new SearchRequest(); searchRequest.allowPartialSearchResults(true); if (randomBoolean()) { From f5a23f0e941b1604c6a2dc0cb7c840ba18d21ccd Mon Sep 17 00:00:00 2001 From: Julie Tibshirani Date: Wed, 2 May 2018 20:54:30 -0700 Subject: [PATCH 3/7] Allow for SearchTemplateResponse to be serialized from XContent. --- .../mustache/SearchTemplateResponse.java | 33 ++- .../mustache/SearchTemplateResponseTests.java | 211 ++++++++++++++++++ 2 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateResponseTests.java diff --git a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/SearchTemplateResponse.java b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/SearchTemplateResponse.java index 792d993915992..500a5a399ef4a 100644 --- a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/SearchTemplateResponse.java +++ b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/SearchTemplateResponse.java @@ -21,18 +21,23 @@ import org.elasticsearch.action.ActionResponse; import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.common.ParseField; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.StatusToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.rest.RestStatus; import java.io.IOException; import java.io.InputStream; +import java.util.Map; -public class SearchTemplateResponse extends ActionResponse implements StatusToXContentObject { +public class SearchTemplateResponse extends ActionResponse implements StatusToXContentObject { + public static ParseField TEMPLATE_OUTPUT_FIELD = new ParseField("template_output"); /** Contains the source of the rendered template **/ private BytesReference source; @@ -77,6 +82,30 @@ public void readFrom(StreamInput in) throws IOException { response = in.readOptionalStreamable(SearchResponse::new); } + public static SearchTemplateResponse fromXContent(XContentParser parser) throws IOException { + SearchTemplateResponse searchTemplateResponse = new SearchTemplateResponse(); + Map contentAsMap = parser.map(); + + if (contentAsMap.containsKey(TEMPLATE_OUTPUT_FIELD.getPreferredName())) { + Object source = contentAsMap.get(TEMPLATE_OUTPUT_FIELD.getPreferredName()); + XContentBuilder builder = XContentFactory.contentBuilder(XContentType.JSON) + .value(source); + searchTemplateResponse.setSource(BytesReference.bytes(builder)); + } else { + XContentType contentType = parser.contentType(); + XContentBuilder builder = XContentFactory.contentBuilder(contentType) + .map(contentAsMap); + XContentParser searchResponseParser = contentType.xContent().createParser( + parser.getXContentRegistry(), + parser.getDeprecationHandler(), + BytesReference.bytes(builder).streamInput()); + + SearchResponse searchResponse = SearchResponse.fromXContent(searchResponseParser); + searchTemplateResponse.setResponse(searchResponse); + } + return searchTemplateResponse; + } + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { if (hasResponse()) { @@ -85,7 +114,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws builder.startObject(); //we can assume the template is always json as we convert it before compiling it try (InputStream stream = source.streamInput()) { - builder.rawField("template_output", stream, XContentType.JSON); + builder.rawField(TEMPLATE_OUTPUT_FIELD.getPreferredName(), stream, XContentType.JSON); } builder.endObject(); } diff --git a/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateResponseTests.java b/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateResponseTests.java new file mode 100644 index 0000000000000..54591ed9c9dd9 --- /dev/null +++ b/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateResponseTests.java @@ -0,0 +1,211 @@ +/* + * 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 org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.action.search.ShardSearchFailure; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.text.Text; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.search.SearchHit; +import org.elasticsearch.search.SearchHits; +import org.elasticsearch.search.internal.InternalSearchResponse; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; +import java.util.Collections; +import java.util.function.Predicate; + +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent; + +public class SearchTemplateResponseTests extends AbstractXContentTestCase { + + @Override + protected SearchTemplateResponse createTestInstance() { + SearchTemplateResponse response = new SearchTemplateResponse(); + if (randomBoolean()) { + response.setResponse(createSearchResponse()); + } else { + response.setSource(createSource()); + } + return response; + } + + @Override + protected SearchTemplateResponse doParseInstance(XContentParser parser) throws IOException { + return SearchTemplateResponse.fromXContent(parser); + } + + /** + * For simplicity we create a minimal response, as there is already a dedicated + * test class for search response parsing and serialization. + */ + private SearchResponse createSearchResponse() { + long tookInMillis = randomNonNegativeLong(); + int totalShards = randomIntBetween(1, Integer.MAX_VALUE); + int successfulShards = randomIntBetween(0, totalShards); + int skippedShards = randomIntBetween(0, totalShards); + InternalSearchResponse internalSearchResponse = InternalSearchResponse.empty(); + + return new SearchResponse(internalSearchResponse, null, totalShards, successfulShards, + skippedShards, tookInMillis, ShardSearchFailure.EMPTY_ARRAY, SearchResponse.Clusters.EMPTY); + } + + private BytesReference createSource() { + try { + XContentBuilder source = XContentFactory.jsonBuilder() + .startObject() + .startObject("query") + .startObject("match") + .field(randomAlphaOfLength(5), randomAlphaOfLength(10)) + .endObject() + .endObject() + .endObject(); + return BytesReference.bytes(source); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + protected Predicate getRandomFieldsExcludeFilter() { + String templateOutputField = SearchTemplateResponse.TEMPLATE_OUTPUT_FIELD.getPreferredName(); + return field -> field.equals(templateOutputField) || field.startsWith(templateOutputField + "."); + } + + /** + * Note that we can't rely on normal equals and hashCode checks, since {@link SearchResponse} doesn't + * currently implement equals and hashCode. Instead, we compare the template outputs for equality, + * and perform some sanity checks on the search response instances. + */ + @Override + protected void assertEqualInstances(SearchTemplateResponse expectedInstance, SearchTemplateResponse newInstance) { + assertNotSame(newInstance, expectedInstance); + + BytesReference expectedSource = expectedInstance.getSource(); + BytesReference newSource = newInstance.getSource(); + assertEquals(expectedSource == null, newSource == null); + if (expectedSource != null) { + try { + assertToXContentEquivalent(expectedSource, newSource, XContentType.JSON); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + assertEquals(expectedInstance.hasResponse(), newInstance.hasResponse()); + if (expectedInstance.hasResponse()) { + SearchResponse expectedResponse = expectedInstance.getResponse(); + SearchResponse newResponse = newInstance.getResponse(); + + assertEquals(expectedResponse.getHits().totalHits, newResponse.getHits().totalHits); + assertEquals(expectedResponse.getHits().getMaxScore(), newResponse.getHits().getMaxScore(), 0.0001); + } + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } + + public void testSourceToXContent() throws IOException { + SearchTemplateResponse response = new SearchTemplateResponse(); + + XContentBuilder source = XContentFactory.jsonBuilder() + .startObject() + .startObject("query") + .startObject("terms") + .field("status", new String[]{"pending", "published"}) + .endObject() + .endObject() + .endObject(); + response.setSource(BytesReference.bytes(source)); + + XContentType contentType = randomFrom(XContentType.values()); + XContentBuilder expectedResponse = XContentFactory.contentBuilder(contentType) + .startObject() + .startObject("template_output") + .startObject("query") + .startObject("terms") + .field("status", new String[]{"pending", "published"}) + .endObject() + .endObject() + .endObject() + .endObject(); + + XContentBuilder actualResponse = XContentFactory.contentBuilder(contentType); + response.toXContent(actualResponse, ToXContent.EMPTY_PARAMS); + + assertToXContentEquivalent( + BytesReference.bytes(expectedResponse), + BytesReference.bytes(actualResponse), + contentType); + } + + public void testSearchResponseToXContent() throws IOException { + SearchHit hit = new SearchHit(1, "id", new Text("type"), Collections.emptyMap()); + hit.score(2.0f); + SearchHit[] hits = new SearchHit[] { hit }; + + InternalSearchResponse internalSearchResponse = new InternalSearchResponse( + new SearchHits(hits, 100, 1.5f), null, null, null, false, null, 1); + SearchResponse searchResponse = new SearchResponse(internalSearchResponse, null, + 0, 0, 0, 0, ShardSearchFailure.EMPTY_ARRAY, SearchResponse.Clusters.EMPTY); + + SearchTemplateResponse response = new SearchTemplateResponse(); + response.setResponse(searchResponse); + + XContentType contentType = randomFrom(XContentType.values()); + XContentBuilder expectedResponse = XContentFactory.contentBuilder(contentType) + .startObject() + .field("took", 0) + .field("timed_out", false) + .startObject("_shards") + .field("total", 0) + .field("successful", 0) + .field("skipped", 0) + .field("failed", 0) + .endObject() + .startObject("hits") + .field("total", 100) + .field("max_score", 1.5F) + .startArray("hits") + .startObject() + .field("_type", "type") + .field("_id", "id") + .field("_score", 2.0F) + .endObject() + .endArray() + .endObject() + .endObject(); + + XContentBuilder actualResponse = XContentFactory.contentBuilder(contentType); + response.toXContent(actualResponse, ToXContent.EMPTY_PARAMS); + + assertToXContentEquivalent( + BytesReference.bytes(expectedResponse), + BytesReference.bytes(actualResponse), + contentType); + } +} From 5bee4c34d748a3f958c2f0f32431bd2d0a2aa3e7 Mon Sep 17 00:00:00 2001 From: Julie Tibshirani Date: Wed, 2 May 2018 13:09:22 -0700 Subject: [PATCH 4/7] Implement RequestConverters#searchTemplate. --- client/rest-high-level/build.gradle | 1 + .../client/RequestConverters.java | 35 ++++- .../client/RequestConvertersTests.java | 125 +++++++++++++----- 3 files changed, 126 insertions(+), 35 deletions(-) diff --git a/client/rest-high-level/build.gradle b/client/rest-high-level/build.gradle index c273e76a92aed..222de9608aeb9 100644 --- a/client/rest-high-level/build.gradle +++ b/client/rest-high-level/build.gradle @@ -40,6 +40,7 @@ dependencies { compile "org.elasticsearch.plugin:parent-join-client:${version}" compile "org.elasticsearch.plugin:aggs-matrix-stats-client:${version}" compile "org.elasticsearch.plugin:rank-eval-client:${version}" + compile "org.elasticsearch.plugin:lang-mustache-client:${version}" testCompile "org.elasticsearch.client:test:${version}" testCompile "org.elasticsearch.test:framework:${version}" diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java index 2e7b4ba74cc39..4d4098ba93222 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java @@ -80,6 +80,7 @@ import org.elasticsearch.index.VersionType; import org.elasticsearch.index.rankeval.RankEvalRequest; import org.elasticsearch.rest.action.search.RestSearchAction; +import org.elasticsearch.script.mustache.SearchTemplateRequest; import org.elasticsearch.search.fetch.subphase.FetchSourceContext; import java.io.ByteArrayOutputStream; @@ -458,6 +459,15 @@ static Request search(SearchRequest searchRequest) throws IOException { Request request = new Request(HttpPost.METHOD_NAME, endpoint(searchRequest.indices(), searchRequest.types(), "_search")); Params params = new Params(request); + addSearchRequestParams(params, searchRequest); + + if (searchRequest.source() != null) { + request.setEntity(createEntity(searchRequest.source(), REQUEST_BODY_CONTENT_TYPE)); + } + return request; + } + + private static void addSearchRequestParams(Params params, SearchRequest searchRequest) { params.putParam(RestSearchAction.TYPED_KEYS_PARAM, "true"); params.withRouting(searchRequest.routing()); params.withPreference(searchRequest.preference()); @@ -473,11 +483,6 @@ static Request search(SearchRequest searchRequest) throws IOException { if (searchRequest.scroll() != null) { params.putParam("scroll", searchRequest.scroll().keepAlive()); } - - if (searchRequest.source() != null) { - request.setEntity(createEntity(searchRequest.source(), REQUEST_BODY_CONTENT_TYPE)); - } - return request; } static Request searchScroll(SearchScrollRequest searchScrollRequest) throws IOException { @@ -507,6 +512,26 @@ static Request multiSearch(MultiSearchRequest multiSearchRequest) throws IOExcep return request; } + static Request searchTemplate(SearchTemplateRequest searchTemplateRequest) throws IOException { + Request request; + + if (searchTemplateRequest.isSimulate()) { + request = new Request(HttpGet.METHOD_NAME, "_render/template"); + } else { + SearchRequest searchRequest = searchTemplateRequest.getRequest(); + assert searchRequest != null : "When not simulating a template request, a search request must be present."; + + String endpoint = endpoint(searchRequest.indices(), searchRequest.types(), "_search/template"); + request = new Request(HttpGet.METHOD_NAME, endpoint); + + Params params = new Params(request); + addSearchRequestParams(params, searchRequest); + } + + request.setEntity(createEntity(searchTemplateRequest, REQUEST_BODY_CONTENT_TYPE)); + return request; + } + static Request existsAlias(GetAliasesRequest getAliasesRequest) { if ((getAliasesRequest.indices() == null || getAliasesRequest.indices().length == 0) && (getAliasesRequest.aliases() == null || getAliasesRequest.aliases().length == 0)) { diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java index 2d4ef8b6413d9..9c75d67e04304 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/RequestConvertersTests.java @@ -95,6 +95,8 @@ import org.elasticsearch.index.rankeval.RatedRequest; import org.elasticsearch.index.rankeval.RestRankEvalAction; import org.elasticsearch.rest.action.search.RestSearchAction; +import org.elasticsearch.script.ScriptType; +import org.elasticsearch.script.mustache.SearchTemplateRequest; import org.elasticsearch.search.Scroll; import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; import org.elasticsearch.search.aggregations.support.ValueType; @@ -1011,36 +1013,7 @@ public void testSearch() throws Exception { searchRequest.types(types); Map expectedParams = new HashMap<>(); - expectedParams.put(RestSearchAction.TYPED_KEYS_PARAM, "true"); - if (randomBoolean()) { - searchRequest.routing(randomAlphaOfLengthBetween(3, 10)); - expectedParams.put("routing", searchRequest.routing()); - } - if (randomBoolean()) { - searchRequest.preference(randomAlphaOfLengthBetween(3, 10)); - expectedParams.put("preference", searchRequest.preference()); - } - if (randomBoolean()) { - searchRequest.searchType(randomFrom(SearchType.values())); - } - expectedParams.put("search_type", searchRequest.searchType().name().toLowerCase(Locale.ROOT)); - if (randomBoolean()) { - searchRequest.requestCache(randomBoolean()); - expectedParams.put("request_cache", Boolean.toString(searchRequest.requestCache())); - } - if (randomBoolean()) { - searchRequest.allowPartialSearchResults(randomBoolean()); - expectedParams.put("allow_partial_search_results", Boolean.toString(searchRequest.allowPartialSearchResults())); - } - if (randomBoolean()) { - searchRequest.setBatchedReduceSize(randomIntBetween(2, Integer.MAX_VALUE)); - } - expectedParams.put("batched_reduce_size", Integer.toString(searchRequest.getBatchedReduceSize())); - if (randomBoolean()) { - searchRequest.scroll(randomTimeValue()); - expectedParams.put("scroll", searchRequest.scroll().keepAlive().getStringRep()); - } - + setRandomSearchParams(searchRequest, expectedParams); setRandomIndicesOptions(searchRequest::indicesOptions, searchRequest::indicesOptions, expectedParams); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); @@ -1189,6 +1162,65 @@ public void testClearScroll() throws IOException { assertEquals(REQUEST_BODY_CONTENT_TYPE.mediaTypeWithoutParameters(), request.getEntity().getContentType().getValue()); } + public void testSearchTemplate() throws Exception { + // Create a random request. + String[] indices = randomIndicesNames(0, 5); + SearchRequest searchRequest = new SearchRequest(indices); + + Map expectedParams = new HashMap<>(); + setRandomSearchParams(searchRequest, expectedParams); + setRandomIndicesOptions(searchRequest::indicesOptions, searchRequest::indicesOptions, expectedParams); + + SearchTemplateRequest searchTemplateRequest = new SearchTemplateRequest(searchRequest); + + searchTemplateRequest.setScript("{\"query\": { \"match\" : { \"{{field}}\" : \"{{value}}\" }}}"); + searchTemplateRequest.setScriptType(ScriptType.INLINE); + searchTemplateRequest.setProfile(randomBoolean()); + + Map scriptParams = new HashMap<>(); + scriptParams.put("field", "name"); + scriptParams.put("value", "soren"); + searchTemplateRequest.setScriptParams(scriptParams); + + // Verify that the resulting REST request looks as expected. + Request request = RequestConverters.searchTemplate(searchTemplateRequest); + StringJoiner endpoint = new StringJoiner("/", "/", ""); + String index = String.join(",", indices); + if (Strings.hasLength(index)) { + endpoint.add(index); + } + endpoint.add("_search/template"); + + assertEquals(HttpGet.METHOD_NAME, request.getMethod()); + assertEquals(endpoint.toString(), request.getEndpoint()); + assertEquals(expectedParams, request.getParameters()); + assertToXContentBody(searchTemplateRequest, request.getEntity()); + } + + public void testRenderSearchTemplate() throws Exception { + // Create a simple request. + SearchTemplateRequest searchTemplateRequest = new SearchTemplateRequest(); + searchTemplateRequest.setSimulate(true); // Setting simulate true means the template should only be rendered. + + searchTemplateRequest.setScript("template1"); + searchTemplateRequest.setScriptType(ScriptType.STORED); + searchTemplateRequest.setProfile(randomBoolean()); + + Map scriptParams = new HashMap<>(); + scriptParams.put("field", "name"); + scriptParams.put("value", "soren"); + searchTemplateRequest.setScriptParams(scriptParams); + + // Verify that the resulting REST request looks as expected. + Request request = RequestConverters.searchTemplate(searchTemplateRequest); + String endpoint = "_render/template"; + + assertEquals(HttpGet.METHOD_NAME, request.getMethod()); + assertEquals(endpoint, request.getEndpoint()); + assertEquals(Collections.emptyMap(), request.getParameters()); + assertToXContentBody(searchTemplateRequest, request.getEntity()); + } + public void testExistsAlias() { GetAliasesRequest getAliasesRequest = new GetAliasesRequest(); String[] indices = randomBoolean() ? null : randomIndicesNames(0, 5); @@ -1662,6 +1694,39 @@ private static void randomizeFetchSourceContextParams(Consumer expectedParams) { + expectedParams.put(RestSearchAction.TYPED_KEYS_PARAM, "true"); + if (randomBoolean()) { + searchRequest.routing(randomAlphaOfLengthBetween(3, 10)); + expectedParams.put("routing", searchRequest.routing()); + } + if (randomBoolean()) { + searchRequest.preference(randomAlphaOfLengthBetween(3, 10)); + expectedParams.put("preference", searchRequest.preference()); + } + if (randomBoolean()) { + searchRequest.searchType(randomFrom(SearchType.values())); + } + expectedParams.put("search_type", searchRequest.searchType().name().toLowerCase(Locale.ROOT)); + if (randomBoolean()) { + searchRequest.requestCache(randomBoolean()); + expectedParams.put("request_cache", Boolean.toString(searchRequest.requestCache())); + } + if (randomBoolean()) { + searchRequest.allowPartialSearchResults(randomBoolean()); + expectedParams.put("allow_partial_search_results", Boolean.toString(searchRequest.allowPartialSearchResults())); + } + if (randomBoolean()) { + searchRequest.setBatchedReduceSize(randomIntBetween(2, Integer.MAX_VALUE)); + } + expectedParams.put("batched_reduce_size", Integer.toString(searchRequest.getBatchedReduceSize())); + if (randomBoolean()) { + searchRequest.scroll(randomTimeValue()); + expectedParams.put("scroll", searchRequest.scroll().keepAlive().getStringRep()); + } + } + private static void setRandomIndicesOptions(Consumer setter, Supplier getter, Map expectedParams) { From 0003b8e4a0466bc9eab4438f1f5127e904a7ce2f Mon Sep 17 00:00:00 2001 From: Julie Tibshirani Date: Fri, 4 May 2018 16:27:09 -0700 Subject: [PATCH 5/7] Add support for search templates to the high-level REST client. --- .../client/RestHighLevelClient.java | 28 +++++ .../org/elasticsearch/client/SearchIT.java | 104 ++++++++++++++++++ 2 files changed, 132 insertions(+) diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java index 1985d6bd06dd4..5dbf2709d9988 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RestHighLevelClient.java @@ -64,6 +64,8 @@ import org.elasticsearch.plugins.spi.NamedXContentProvider; import org.elasticsearch.rest.BytesRestResponse; import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.script.mustache.SearchTemplateRequest; +import org.elasticsearch.script.mustache.SearchTemplateResponse; import org.elasticsearch.search.aggregations.Aggregation; import org.elasticsearch.search.aggregations.bucket.adjacency.AdjacencyMatrixAggregationBuilder; import org.elasticsearch.search.aggregations.bucket.adjacency.ParsedAdjacencyMatrix; @@ -501,6 +503,32 @@ public final void clearScrollAsync(ClearScrollRequest clearScrollRequest, listener, emptySet(), headers); } + /** + * Executes a request using the Search Template API. + * + * See Search Template API + * on elastic.co. + */ + public final SearchTemplateResponse searchTemplate(SearchTemplateRequest searchTemplateRequest, + Header... headers) throws IOException { + return performRequestAndParseEntity(searchTemplateRequest, RequestConverters::searchTemplate, + SearchTemplateResponse::fromXContent, emptySet(), headers); + } + + /** + * Asynchronously executes a request using the Search Template API + * + * See Search Template API + * on elastic.co. + */ + public final void searchTemplateAsync(SearchTemplateRequest searchTemplateRequest, + ActionListener listener, + Header... headers) { + performRequestAsyncAndParseEntity(searchTemplateRequest, RequestConverters::searchTemplate, + SearchTemplateResponse::fromXContent, listener, emptySet(), headers); + } + + /** * Executes a request using the Ranking Evaluation API. * diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/SearchIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/SearchIT.java index 549b4ce0a85c5..e147642fc73bd 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/SearchIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/SearchIT.java @@ -38,8 +38,11 @@ import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.SearchScrollRequest; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.query.MatchQueryBuilder; import org.elasticsearch.index.query.ScriptQueryBuilder; import org.elasticsearch.index.query.TermsQueryBuilder; @@ -48,6 +51,8 @@ import org.elasticsearch.rest.RestStatus; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptType; +import org.elasticsearch.script.mustache.SearchTemplateRequest; +import org.elasticsearch.script.mustache.SearchTemplateResponse; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.aggregations.BucketOrder; import org.elasticsearch.search.aggregations.bucket.range.Range; @@ -69,10 +74,12 @@ import java.io.IOException; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent; import static org.hamcrest.Matchers.both; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.either; @@ -733,6 +740,103 @@ public void testMultiSearch_failure() throws Exception { assertThat(multiSearchResponse.getResponses()[1].getResponse(), nullValue()); } + public void testSearchTemplate() throws IOException { + SearchTemplateRequest searchTemplateRequest = new SearchTemplateRequest(); + searchTemplateRequest.setRequest(new SearchRequest("index")); + + searchTemplateRequest.setScriptType(ScriptType.INLINE); + searchTemplateRequest.setScript( + "{" + + " \"query\": {" + + " \"match\": {" + + " \"num\": {{number}}" + + " }" + + " }" + + "}"); + + Map scriptParams = new HashMap<>(); + scriptParams.put("number", 10); + searchTemplateRequest.setScriptParams(scriptParams); + + searchTemplateRequest.setExplain(true); + searchTemplateRequest.setProfile(true); + + SearchTemplateResponse searchTemplateResponse = execute(searchTemplateRequest, + highLevelClient()::searchTemplate, + highLevelClient()::searchTemplateAsync); + + assertNull(searchTemplateResponse.getSource()); + + SearchResponse searchResponse = searchTemplateResponse.getResponse(); + assertNotNull(searchResponse); + + assertEquals(1, searchResponse.getHits().totalHits); + assertEquals(1, searchResponse.getHits().getHits().length); + assertThat(searchResponse.getHits().getMaxScore(), greaterThan(0f)); + + SearchHit hit = searchResponse.getHits().getHits()[0]; + assertNotNull(hit.getExplanation()); + + assertFalse(searchResponse.getProfileResults().isEmpty()); + } + + public void testNonExistentSearchTemplate() { + SearchTemplateRequest searchTemplateRequest = new SearchTemplateRequest(); + searchTemplateRequest.setRequest(new SearchRequest("index")); + + searchTemplateRequest.setScriptType(ScriptType.STORED); + searchTemplateRequest.setScript("non-existent"); + searchTemplateRequest.setScriptParams(Collections.emptyMap()); + + ElasticsearchStatusException exception = expectThrows(ElasticsearchStatusException.class, + () -> execute(searchTemplateRequest, + highLevelClient()::searchTemplate, + highLevelClient()::searchTemplateAsync)); + + assertEquals(RestStatus.NOT_FOUND, exception.status()); + } + + public void testRenderSearchTemplate() throws IOException { + SearchTemplateRequest searchTemplateRequest = new SearchTemplateRequest(); + + searchTemplateRequest.setScriptType(ScriptType.INLINE); + searchTemplateRequest.setScript( + "{" + + " \"query\": {" + + " \"match\": {" + + " \"num\": {{number}}" + + " }" + + " }" + + "}"); + + Map scriptParams = new HashMap<>(); + scriptParams.put("number", 10); + searchTemplateRequest.setScriptParams(scriptParams); + + // Setting simulate true causes the template to only be rendered. + searchTemplateRequest.setSimulate(true); + + SearchTemplateResponse searchTemplateResponse = execute(searchTemplateRequest, + highLevelClient()::searchTemplate, + highLevelClient()::searchTemplateAsync); + assertNull(searchTemplateResponse.getResponse()); + + BytesReference expectedSource = BytesReference.bytes( + XContentFactory.jsonBuilder() + .startObject() + .startObject("query") + .startObject("match") + .field("num", 10) + .endObject() + .endObject() + .endObject()); + + BytesReference actualSource = searchTemplateResponse.getSource(); + assertNotNull(actualSource); + + assertToXContentEquivalent(expectedSource, actualSource, XContentType.JSON); + } + public void testFieldCaps() throws IOException { FieldCapabilitiesRequest request = new FieldCapabilitiesRequest() .indices("index1", "index2") From ae648c38359527cd7f0302923dfdfc5ee57ed1e9 Mon Sep 17 00:00:00 2001 From: Julie Tibshirani Date: Fri, 4 May 2018 16:32:23 -0700 Subject: [PATCH 6/7] Add documentation. --- .../documentation/SearchDocumentationIT.java | 129 ++++++++++++++++++ .../search/search-template.asciidoc | 118 ++++++++++++++++ .../high-level/supported-apis.asciidoc | 2 + 3 files changed, 249 insertions(+) create mode 100644 docs/java-rest/high-level/search/search-template.asciidoc diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SearchDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SearchDocumentationIT.java index 8a12016025c3e..db90b802a2833 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SearchDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SearchDocumentationIT.java @@ -19,6 +19,8 @@ package org.elasticsearch.client.documentation; +import org.apache.http.entity.ContentType; +import org.apache.http.nio.entity.NStringEntity; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.LatchedActionListener; import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; @@ -41,7 +43,11 @@ import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.client.ESRestHighLevelClientTestCase; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.RestClient; import org.elasticsearch.client.RestHighLevelClient; +import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.text.Text; import org.elasticsearch.common.unit.Fuzziness; import org.elasticsearch.common.unit.TimeValue; @@ -60,6 +66,9 @@ import org.elasticsearch.index.rankeval.RatedRequest; import org.elasticsearch.index.rankeval.RatedSearchHit; import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.script.ScriptType; +import org.elasticsearch.script.mustache.SearchTemplateRequest; +import org.elasticsearch.script.mustache.SearchTemplateResponse; import org.elasticsearch.search.Scroll; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHits; @@ -92,6 +101,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; @@ -706,9 +716,128 @@ public void onFailure(Exception e) { } } + public void testSearchTemplateWithInlineScript() throws Exception { + indexSearchTestData(); + RestHighLevelClient client = highLevelClient(); + + // tag::search-template-request-inline + SearchTemplateRequest request = new SearchTemplateRequest(); + request.setRequest(new SearchRequest("posts")); // <1> + + request.setScriptType(ScriptType.INLINE); + request.setScript( // <2> + "{" + + " \"query\": { \"match\" : { \"{{field}}\" : \"{{value}}\" } }," + + " \"size\" : \"{{size}}\"" + + "}"); + + Map scriptParams = new HashMap<>(); + scriptParams.put("field", "title"); + scriptParams.put("value", "elasticsearch"); + scriptParams.put("size", 5); + request.setScriptParams(scriptParams); // <3> + // end::search-template-request-inline + + // tag::search-template-response + SearchTemplateResponse response = client.searchTemplate(request); + SearchResponse searchResponse = response.getResponse(); + assertNotNull(searchResponse); + assertTrue(searchResponse.getHits().totalHits > 0); + // end::search-template-response + + // tag::render-search-template-request + request.setSimulate(true); // <1> + // end::render-search-template-request + + // tag::render-search-template-response + SearchTemplateResponse renderResponse = client.searchTemplate(request); + BytesReference source = renderResponse.getSource(); + assertNotNull(source); + assertEquals(( + "{" + + " \"size\" : \"5\"," + + " \"query\": { \"match\" : { \"title\" : \"elasticsearch\" } }" + + "}").replaceAll("\\s+", ""), source.utf8ToString()); + // end::render-search-template-response + } + + public void testSearchTemplateWithStoredScript() throws Exception { + indexSearchTestData(); + RestHighLevelClient client = highLevelClient(); + RestClient restClient = client(); + + // tag::register-script + Request scriptRequest = new Request("POST", "_scripts/title_search"); + scriptRequest.setEntity(new NStringEntity( + "{" + + " \"script\": {" + + " \"lang\": \"mustache\"," + + " \"source\": {" + + " \"query\": { \"match\" : { \"{{field}}\" : \"{{value}}\" } }," + + " \"size\" : \"{{size}}\"" + + " }" + + " }" + + "}", ContentType.APPLICATION_JSON)); + Response scriptResponse = restClient.performRequest(scriptRequest); + assertEquals(RestStatus.OK.getStatus(), scriptResponse.getStatusLine().getStatusCode()); + // end::register-script + + // tag::search-template-request-stored + SearchTemplateRequest request = new SearchTemplateRequest(); + request.setRequest(new SearchRequest("posts")); + + request.setScriptType(ScriptType.STORED); + request.setScript("title_search"); + + Map params = new HashMap<>(); + params.put("field", "title"); + params.put("value", "elasticsearch"); + params.put("size", 5); + request.setScriptParams(params); + // end::search-template-request-stored + + // tag::search-template-request-options + request.setExplain(true); + request.setProfile(true); + // end::search-template-request-options + + // tag::search-template-execute + SearchTemplateResponse response = client.searchTemplate(request); + // end::search-template-execute + + SearchResponse searchResponse = response.getResponse(); + assertNotNull(searchResponse); + assertTrue(searchResponse.getHits().totalHits > 0); + + // tag::search-template-execute-listener + ActionListener listener = new ActionListener() { + @Override + public void onResponse(SearchTemplateResponse response) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::search-template-execute-listener + + // Replace the empty listener by a blocking listener for tests. + CountDownLatch latch = new CountDownLatch(1); + listener = new LatchedActionListener<>(listener, latch); + + // tag::search-template-execute-async + client.searchTemplateAsync(request, listener); // <1> + // end::search-template-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + public void testFieldCaps() throws Exception { indexSearchTestData(); RestHighLevelClient client = highLevelClient(); + // tag::field-caps-request FieldCapabilitiesRequest request = new FieldCapabilitiesRequest() .fields("user") diff --git a/docs/java-rest/high-level/search/search-template.asciidoc b/docs/java-rest/high-level/search/search-template.asciidoc new file mode 100644 index 0000000000000..434c71110b7c9 --- /dev/null +++ b/docs/java-rest/high-level/search/search-template.asciidoc @@ -0,0 +1,118 @@ +[[java-rest-high-search-template]] +=== Search Template API + +The search template API allows for searches to be executed from a template based +on the mustache language, and also for previewing rendered templates. + +[[java-rest-high-search-template-request]] +==== Search Template Request + +===== Inline Templates + +In the most basic form of request, the search template is specified inline: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SearchDocumentationIT.java[search-template-request-inline] +-------------------------------------------------- +<1> The search is executed against the `posts` index. +<2> The template defines the structure of the search source. It is passed +as a string because mustache templates are not always valid JSON. +<3> Before running the search, the template is rendered with the provided parameters. + +===== Registered Templates + +Search templates can be registered in advance through stored scripts API. Note that +the stored scripts API is not yet available in the high-level REST client, so in this +example we use the simple REST client. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SearchDocumentationIT.java[register-script] +-------------------------------------------------- + +Instead of providing an inline script, we can refer to this registered template in the request: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SearchDocumentationIT.java[search-template-request-stored] +-------------------------------------------------- + +===== Rendering Templates + +Given parameter values, a template can be rendered without executing a search: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SearchDocumentationIT.java[render-search-template-request] +-------------------------------------------------- +<1> Setting `simulate` to `true` causes the search template to only be rendered. + +Both inline and pre-registered templates can be rendered. + +===== Optional Arguments + +As in standard search requests, the `explain` and `profile` options are supported: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SearchDocumentationIT.java[search-template-request-options] +-------------------------------------------------- + +===== Additional References + +The https://www.elastic.co/guide/en/elasticsearch/reference/current/search-template.html#search-template[Search Template HTTP documentation] +contains further examples of how search requests can be templated. For more detailed information on how mustache +works and what can be done with it, please consult the http://mustache.github.io/mustache.5.html[online documentation of the mustache project]. + +[[java-rest-high-search-template-sync]] +==== Synchronous Execution + +The `searchTemplate` method executes the request synchronously: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SearchDocumentationIT.java[search-template-execute] +-------------------------------------------------- + +==== Asynchronous Execution + +A search template request can be executed asynchronously through the `searchTemplateAsync` +method: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SearchDocumentationIT.java[search-template-execute-async] +-------------------------------------------------- +<1> The `SearchTemplateRequest` to execute and the `ActionListener` to call when the execution completes. + +The asynchronous method does not block and returns immediately. Once the request completes, the +`ActionListener` is called back using the `onResponse` method if the execution completed successfully, +or using the `onFailure` method if it failed. + +A typical listener for `SearchTemplateResponse` is constructed as follows: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SearchDocumentationIT.java[search-template-execute-listener] +-------------------------------------------------- +<1> Called when the execution is successfully completed. +<2> Called when the whole `SearchTemplateRequest` fails. + +==== Search Template Response + +For a standard search template request, the response contains a `SearchResponse` object +with the result of executing the search: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SearchDocumentationIT.java[search-template-response] +-------------------------------------------------- + +If `simulate` was set to `true` in the request, then the response +will contain the rendered search source instead of a `SearchResponse`: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/SearchDocumentationIT.java[render-search-template-response] +-------------------------------------------------- diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index 2dee4643e73eb..62e65ec650bca 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -31,6 +31,7 @@ The Java High Level REST Client supports the following Search APIs: * <> * <> * <> +* <> * <> * <> * <> @@ -38,6 +39,7 @@ The Java High Level REST Client supports the following Search APIs: include::search/search.asciidoc[] include::search/scroll.asciidoc[] include::search/multi-search.asciidoc[] +include::search/search-template.asciidoc[] include::search/field-caps.asciidoc[] include::search/rank-eval.asciidoc[] From 776ed0208b8d25f8b9008a50d0898b7e6f8c899f Mon Sep 17 00:00:00 2001 From: Julie Tibshirani Date: Mon, 14 May 2018 13:49:02 -0700 Subject: [PATCH 7/7] Address code review feedback. --- .../client/RequestConverters.java | 2 - .../documentation/SearchDocumentationIT.java | 16 +- .../search/search-template.asciidoc | 7 +- .../mustache/SearchTemplateRequest.java | 2 +- .../mustache/SearchTemplateRequestTests.java | 180 ++-------------- .../SearchTemplateRequestXContentTests.java | 197 ++++++++++++++++++ .../mustache/SearchTemplateResponseTests.java | 4 +- 7 files changed, 224 insertions(+), 184 deletions(-) create mode 100644 modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateRequestXContentTests.java diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java index 4d4098ba93222..310aafcb6b817 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/RequestConverters.java @@ -519,8 +519,6 @@ static Request searchTemplate(SearchTemplateRequest searchTemplateRequest) throw request = new Request(HttpGet.METHOD_NAME, "_render/template"); } else { SearchRequest searchRequest = searchTemplateRequest.getRequest(); - assert searchRequest != null : "When not simulating a template request, a search request must be present."; - String endpoint = endpoint(searchRequest.indices(), searchRequest.types(), "_search/template"); request = new Request(HttpGet.METHOD_NAME, endpoint); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SearchDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SearchDocumentationIT.java index db90b802a2833..463c5f7d12f5e 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SearchDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SearchDocumentationIT.java @@ -19,8 +19,6 @@ package org.elasticsearch.client.documentation; -import org.apache.http.entity.ContentType; -import org.apache.http.nio.entity.NStringEntity; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.LatchedActionListener; import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; @@ -741,9 +739,10 @@ public void testSearchTemplateWithInlineScript() throws Exception { // tag::search-template-response SearchTemplateResponse response = client.searchTemplate(request); SearchResponse searchResponse = response.getResponse(); + // end::search-template-response + assertNotNull(searchResponse); assertTrue(searchResponse.getHits().totalHits > 0); - // end::search-template-response // tag::render-search-template-request request.setSimulate(true); // <1> @@ -751,14 +750,15 @@ public void testSearchTemplateWithInlineScript() throws Exception { // tag::render-search-template-response SearchTemplateResponse renderResponse = client.searchTemplate(request); - BytesReference source = renderResponse.getSource(); + BytesReference source = renderResponse.getSource(); // <1> + // end::render-search-template-response + assertNotNull(source); assertEquals(( "{" + " \"size\" : \"5\"," + " \"query\": { \"match\" : { \"title\" : \"elasticsearch\" } }" + "}").replaceAll("\\s+", ""), source.utf8ToString()); - // end::render-search-template-response } public void testSearchTemplateWithStoredScript() throws Exception { @@ -768,7 +768,7 @@ public void testSearchTemplateWithStoredScript() throws Exception { // tag::register-script Request scriptRequest = new Request("POST", "_scripts/title_search"); - scriptRequest.setEntity(new NStringEntity( + scriptRequest.setJsonEntity( "{" + " \"script\": {" + " \"lang\": \"mustache\"," + @@ -777,10 +777,10 @@ public void testSearchTemplateWithStoredScript() throws Exception { " \"size\" : \"{{size}}\"" + " }" + " }" + - "}", ContentType.APPLICATION_JSON)); + "}"); Response scriptResponse = restClient.performRequest(scriptRequest); - assertEquals(RestStatus.OK.getStatus(), scriptResponse.getStatusLine().getStatusCode()); // end::register-script + assertEquals(RestStatus.OK.getStatus(), scriptResponse.getStatusLine().getStatusCode()); // tag::search-template-request-stored SearchTemplateRequest request = new SearchTemplateRequest(); diff --git a/docs/java-rest/high-level/search/search-template.asciidoc b/docs/java-rest/high-level/search/search-template.asciidoc index 434c71110b7c9..3f0dfb8ab28e0 100644 --- a/docs/java-rest/high-level/search/search-template.asciidoc +++ b/docs/java-rest/high-level/search/search-template.asciidoc @@ -24,7 +24,7 @@ as a string because mustache templates are not always valid JSON. Search templates can be registered in advance through stored scripts API. Note that the stored scripts API is not yet available in the high-level REST client, so in this -example we use the simple REST client. +example we use the low-level REST client. ["source","java",subs="attributes,callouts,macros"] -------------------------------------------------- @@ -61,9 +61,7 @@ include-tagged::{doc-tests}/SearchDocumentationIT.java[search-template-request-o ===== Additional References -The https://www.elastic.co/guide/en/elasticsearch/reference/current/search-template.html#search-template[Search Template HTTP documentation] -contains further examples of how search requests can be templated. For more detailed information on how mustache -works and what can be done with it, please consult the http://mustache.github.io/mustache.5.html[online documentation of the mustache project]. +The {ref}/search-template.html[Search Template documentation] contains further examples of how search requests can be templated. [[java-rest-high-search-template-sync]] ==== Synchronous Execution @@ -116,3 +114,4 @@ will contain the rendered search source instead of a `SearchResponse`: -------------------------------------------------- include-tagged::{doc-tests}/SearchDocumentationIT.java[render-search-template-response] -------------------------------------------------- +<1> The rendered source in bytes, in our example `{"query": { "match" : { "title" : "elasticsearch" }}, "size" : 5}`. diff --git a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/SearchTemplateRequest.java b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/SearchTemplateRequest.java index 5e76038076a6e..da3cc3688149c 100644 --- a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/SearchTemplateRequest.java +++ b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/SearchTemplateRequest.java @@ -208,7 +208,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws } else if (scriptType == ScriptType.INLINE) { builder.field(SOURCE_FIELD.getPreferredName(), script); } else { - throw new IllegalArgumentException("Unrecognized script type [" + scriptType + "]."); + throw new UnsupportedOperationException("Unrecognized script type [" + scriptType + "]."); } return builder.field(PARAMS_FIELD.getPreferredName(), scriptParams) diff --git a/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateRequestTests.java b/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateRequestTests.java index 4d52815909ab1..7d4a6479727e2 100644 --- a/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateRequestTests.java +++ b/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateRequestTests.java @@ -19,19 +19,10 @@ package org.elasticsearch.script.mustache; -import org.elasticsearch.common.bytes.BytesReference; -import org.elasticsearch.common.xcontent.ToXContent; -import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentFactory; -import org.elasticsearch.common.xcontent.XContentParseException; -import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.common.xcontent.XContentType; -import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.script.ScriptType; import org.elasticsearch.search.RandomSearchRequestGenerator; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.test.AbstractStreamableTestCase; -import org.elasticsearch.test.AbstractXContentTestCase; import java.io.IOException; import java.util.ArrayList; @@ -40,11 +31,6 @@ import java.util.Map; import java.util.function.Consumer; -import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.hasEntry; -import static org.hamcrest.Matchers.nullValue; - public class SearchTemplateRequestTests extends AbstractStreamableTestCase { @Override @@ -54,23 +40,7 @@ protected SearchTemplateRequest createBlankInstance() { @Override protected SearchTemplateRequest createTestInstance() { - SearchTemplateRequest request = new SearchTemplateRequest(); - request.setScriptType(randomFrom(ScriptType.values())); - request.setScript(randomAlphaOfLength(50)); - - Map scriptParams = new HashMap<>(); - for (int i = 0; i < randomInt(10); i++) { - scriptParams.put(randomAlphaOfLength(5), randomAlphaOfLength(10)); - } - request.setScriptParams(scriptParams); - - request.setExplain(randomBoolean()); - request.setProfile(randomBoolean()); - request.setSimulate(randomBoolean()); - - request.setRequest(RandomSearchRequestGenerator.randomSearchRequest( - SearchSourceBuilder::searchSource)); - return request; + return createRandomRequest(); } @Override @@ -102,148 +72,24 @@ protected SearchTemplateRequest mutateInstance(SearchTemplateRequest instance) t return mutatedInstance; } - public void testToXContentWithInlineTemplate() throws IOException { - SearchTemplateRequest request = new SearchTemplateRequest(); - request.setScriptType(ScriptType.INLINE); - request.setScript("{\"query\": { \"match\" : { \"{{my_field}}\" : \"{{my_value}}\" } } }"); - request.setProfile(true); + public static SearchTemplateRequest createRandomRequest() { + SearchTemplateRequest request = new SearchTemplateRequest(); + request.setScriptType(randomFrom(ScriptType.values())); + request.setScript(randomAlphaOfLength(50)); Map scriptParams = new HashMap<>(); - scriptParams.put("my_field", "foo"); - scriptParams.put("my_value", "bar"); + for (int i = 0; i < randomInt(10); i++) { + scriptParams.put(randomAlphaOfLength(5), randomAlphaOfLength(10)); + } request.setScriptParams(scriptParams); - XContentType contentType = randomFrom(XContentType.values()); - XContentBuilder expectedRequest = XContentFactory.contentBuilder(contentType) - .startObject() - .field("source", "{\"query\": { \"match\" : { \"{{my_field}}\" : \"{{my_value}}\" } } }") - .startObject("params") - .field("my_field", "foo") - .field("my_value", "bar") - .endObject() - .field("explain", false) - .field("profile", true) - .endObject(); - - XContentBuilder actualRequest = XContentFactory.contentBuilder(contentType); - request.toXContent(actualRequest, ToXContent.EMPTY_PARAMS); - - assertToXContentEquivalent(BytesReference.bytes(expectedRequest), - BytesReference.bytes(actualRequest), - contentType); - } - - public void testToXContentWithStoredTemplate() throws IOException { - SearchTemplateRequest request = new SearchTemplateRequest(); - - request.setScriptType(ScriptType.STORED); - request.setScript("match_template"); - request.setExplain(true); - - Map params = new HashMap<>(); - params.put("my_field", "foo"); - params.put("my_value", "bar"); - request.setScriptParams(params); - - XContentType contentType = randomFrom(XContentType.values()); - XContentBuilder expectedRequest = XContentFactory.contentBuilder(contentType) - .startObject() - .field("id", "match_template") - .startObject("params") - .field("my_field", "foo") - .field("my_value", "bar") - .endObject() - .field("explain", true) - .field("profile", false) - .endObject(); - - XContentBuilder actualRequest = XContentFactory.contentBuilder(contentType); - request.toXContent(actualRequest, ToXContent.EMPTY_PARAMS); - - assertToXContentEquivalent( - BytesReference.bytes(expectedRequest), - BytesReference.bytes(actualRequest), - contentType); - } - - /** - * Note that for xContent parsing, we omit two parts of the request: - * - The 'simulate' option is always held constant, since this parameter is not included - * in the request's xContent (it's instead used to determine the request endpoint). - * - We omit the random SearchRequest, since this component only affects the request - * parameters and also isn't captured in the request's xContent. - */ - public void testFromXContent() throws IOException { - AbstractXContentTestCase.testFromXContent(NUMBER_OF_TEST_RUNS, - this::createTestInstanceForXContent, - false, - new String[]{}, field -> false, - this::createParser, - SearchTemplateRequest::fromXContent, - this::assertEqualInstances, true); - } + request.setExplain(randomBoolean()); + request.setProfile(randomBoolean()); + request.setSimulate(randomBoolean()); - private SearchTemplateRequest createTestInstanceForXContent() { - SearchTemplateRequest request = createTestInstance(); - request.setSimulate(false); - request.setRequest(null); + request.setRequest(RandomSearchRequestGenerator.randomSearchRequest( + SearchSourceBuilder::searchSource)); return request; } - - public void testFromXContentWithEmbeddedTemplate() throws Exception { - String source = "{" + - " 'source' : {\n" + - " 'query': {\n" + - " 'terms': {\n" + - " 'status': [\n" + - " '{{#status}}',\n" + - " '{{.}}',\n" + - " '{{/status}}'\n" + - " ]\n" + - " }\n" + - " }\n" + - " }" + - "}"; - - SearchTemplateRequest request = SearchTemplateRequest.fromXContent(newParser(source)); - assertThat(request.getScript(), equalTo("{\"query\":{\"terms\":{\"status\":[\"{{#status}}\",\"{{.}}\",\"{{/status}}\"]}}}")); - assertThat(request.getScriptType(), equalTo(ScriptType.INLINE)); - assertThat(request.getScriptParams(), nullValue()); - } - - public void testFromXContentWithEmbeddedTemplateAndParams() throws Exception { - String source = "{" + - " 'source' : {" + - " 'query': { 'match' : { '{{my_field}}' : '{{my_value}}' } }," + - " 'size' : '{{my_size}}'" + - " }," + - " 'params' : {" + - " 'my_field' : 'foo'," + - " 'my_value' : 'bar'," + - " 'my_size' : 5" + - " }" + - "}"; - - SearchTemplateRequest request = SearchTemplateRequest.fromXContent(newParser(source)); - assertThat(request.getScript(), equalTo("{\"query\":{\"match\":{\"{{my_field}}\":\"{{my_value}}\"}},\"size\":\"{{my_size}}\"}")); - assertThat(request.getScriptType(), equalTo(ScriptType.INLINE)); - assertThat(request.getScriptParams().size(), equalTo(3)); - assertThat(request.getScriptParams(), hasEntry("my_field", "foo")); - assertThat(request.getScriptParams(), hasEntry("my_value", "bar")); - assertThat(request.getScriptParams(), hasEntry("my_size", 5)); - } - - public void testFromXContentWithMalformedRequest() { - // Unclosed template id - expectThrows(XContentParseException.class, () -> SearchTemplateRequest.fromXContent(newParser("{'id' : 'another_temp }"))); - } - - /** - * Creates a {@link XContentParser} with the given String while replacing single quote to double quotes. - */ - private XContentParser newParser(String s) throws IOException { - assertNotNull(s); - return createParser(JsonXContent.jsonXContent, s.replace("'", "\"")); - } } diff --git a/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateRequestXContentTests.java b/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateRequestXContentTests.java new file mode 100644 index 0000000000000..0e9e8ca628975 --- /dev/null +++ b/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateRequestXContentTests.java @@ -0,0 +1,197 @@ +/* + * 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 org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParseException; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.script.ScriptType; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertToXContentEquivalent; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.nullValue; + +public class SearchTemplateRequestXContentTests extends AbstractXContentTestCase { + + @Override + public SearchTemplateRequest createTestInstance() { + return SearchTemplateRequestTests.createRandomRequest(); + } + + @Override + protected SearchTemplateRequest doParseInstance(XContentParser parser) throws IOException { + return SearchTemplateRequest.fromXContent(parser); + } + + /** + * Note that when checking equality for xContent parsing, we omit two parts of the request: + * - The 'simulate' option, since this parameter is not included in the + * request's xContent (it's instead used to determine the request endpoint). + * - The random SearchRequest, since this component only affects the request + * parameters and also isn't captured in the request's xContent. + */ + @Override + protected void assertEqualInstances(SearchTemplateRequest expectedInstance, SearchTemplateRequest newInstance) { + assertTrue( + expectedInstance.isExplain() == newInstance.isExplain() && + expectedInstance.isProfile() == newInstance.isProfile() && + expectedInstance.getScriptType() == newInstance.getScriptType() && + Objects.equals(expectedInstance.getScript(), newInstance.getScript()) && + Objects.equals(expectedInstance.getScriptParams(), newInstance.getScriptParams())); + } + + @Override + protected boolean supportsUnknownFields() { + return false; + } + + public void testToXContentWithInlineTemplate() throws IOException { + SearchTemplateRequest request = new SearchTemplateRequest(); + + request.setScriptType(ScriptType.INLINE); + request.setScript("{\"query\": { \"match\" : { \"{{my_field}}\" : \"{{my_value}}\" } } }"); + request.setProfile(true); + + Map scriptParams = new HashMap<>(); + scriptParams.put("my_field", "foo"); + scriptParams.put("my_value", "bar"); + request.setScriptParams(scriptParams); + + XContentType contentType = randomFrom(XContentType.values()); + XContentBuilder expectedRequest = XContentFactory.contentBuilder(contentType) + .startObject() + .field("source", "{\"query\": { \"match\" : { \"{{my_field}}\" : \"{{my_value}}\" } } }") + .startObject("params") + .field("my_field", "foo") + .field("my_value", "bar") + .endObject() + .field("explain", false) + .field("profile", true) + .endObject(); + + XContentBuilder actualRequest = XContentFactory.contentBuilder(contentType); + request.toXContent(actualRequest, ToXContent.EMPTY_PARAMS); + + assertToXContentEquivalent(BytesReference.bytes(expectedRequest), + BytesReference.bytes(actualRequest), + contentType); + } + + public void testToXContentWithStoredTemplate() throws IOException { + SearchTemplateRequest request = new SearchTemplateRequest(); + + request.setScriptType(ScriptType.STORED); + request.setScript("match_template"); + request.setExplain(true); + + Map params = new HashMap<>(); + params.put("my_field", "foo"); + params.put("my_value", "bar"); + request.setScriptParams(params); + + XContentType contentType = randomFrom(XContentType.values()); + XContentBuilder expectedRequest = XContentFactory.contentBuilder(contentType) + .startObject() + .field("id", "match_template") + .startObject("params") + .field("my_field", "foo") + .field("my_value", "bar") + .endObject() + .field("explain", true) + .field("profile", false) + .endObject(); + + XContentBuilder actualRequest = XContentFactory.contentBuilder(contentType); + request.toXContent(actualRequest, ToXContent.EMPTY_PARAMS); + + assertToXContentEquivalent( + BytesReference.bytes(expectedRequest), + BytesReference.bytes(actualRequest), + contentType); + } + + public void testFromXContentWithEmbeddedTemplate() throws Exception { + String source = "{" + + " 'source' : {\n" + + " 'query': {\n" + + " 'terms': {\n" + + " 'status': [\n" + + " '{{#status}}',\n" + + " '{{.}}',\n" + + " '{{/status}}'\n" + + " ]\n" + + " }\n" + + " }\n" + + " }" + + "}"; + + SearchTemplateRequest request = SearchTemplateRequest.fromXContent(newParser(source)); + assertThat(request.getScript(), equalTo("{\"query\":{\"terms\":{\"status\":[\"{{#status}}\",\"{{.}}\",\"{{/status}}\"]}}}")); + assertThat(request.getScriptType(), equalTo(ScriptType.INLINE)); + assertThat(request.getScriptParams(), nullValue()); + } + + public void testFromXContentWithEmbeddedTemplateAndParams() throws Exception { + String source = "{" + + " 'source' : {" + + " 'query': { 'match' : { '{{my_field}}' : '{{my_value}}' } }," + + " 'size' : '{{my_size}}'" + + " }," + + " 'params' : {" + + " 'my_field' : 'foo'," + + " 'my_value' : 'bar'," + + " 'my_size' : 5" + + " }" + + "}"; + + SearchTemplateRequest request = SearchTemplateRequest.fromXContent(newParser(source)); + assertThat(request.getScript(), equalTo("{\"query\":{\"match\":{\"{{my_field}}\":\"{{my_value}}\"}},\"size\":\"{{my_size}}\"}")); + assertThat(request.getScriptType(), equalTo(ScriptType.INLINE)); + assertThat(request.getScriptParams().size(), equalTo(3)); + assertThat(request.getScriptParams(), hasEntry("my_field", "foo")); + assertThat(request.getScriptParams(), hasEntry("my_value", "bar")); + assertThat(request.getScriptParams(), hasEntry("my_size", 5)); + } + + public void testFromXContentWithMalformedRequest() { + // Unclosed template id + expectThrows(XContentParseException.class, () -> SearchTemplateRequest.fromXContent(newParser("{'id' : 'another_temp }"))); + } + + /** + * Creates a {@link XContentParser} with the given String while replacing single quote to double quotes. + */ + private XContentParser newParser(String s) throws IOException { + assertNotNull(s); + return createParser(JsonXContent.jsonXContent, s.replace("'", "\"")); + } +} diff --git a/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateResponseTests.java b/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateResponseTests.java index 54591ed9c9dd9..53f5d1d8f842e 100644 --- a/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateResponseTests.java +++ b/modules/lang-mustache/src/test/java/org/elasticsearch/script/mustache/SearchTemplateResponseTests.java @@ -61,7 +61,7 @@ protected SearchTemplateResponse doParseInstance(XContentParser parser) throws I * For simplicity we create a minimal response, as there is already a dedicated * test class for search response parsing and serialization. */ - private SearchResponse createSearchResponse() { + private static SearchResponse createSearchResponse() { long tookInMillis = randomNonNegativeLong(); int totalShards = randomIntBetween(1, Integer.MAX_VALUE); int successfulShards = randomIntBetween(0, totalShards); @@ -72,7 +72,7 @@ private SearchResponse createSearchResponse() { skippedShards, tookInMillis, ShardSearchFailure.EMPTY_ARRAY, SearchResponse.Clusters.EMPTY); } - private BytesReference createSource() { + private static BytesReference createSource() { try { XContentBuilder source = XContentFactory.jsonBuilder() .startObject()