From d3c62e9a704513a15a6525306788faf5c59651c9 Mon Sep 17 00:00:00 2001 From: Colin Goodheart-Smithe Date: Tue, 9 Jun 2015 08:31:01 +0100 Subject: [PATCH] Search Templates: Adds API endpoint to render search templates as a response Closes #6821 --- .settings/org.eclipse.core.resources.prefs | 1 - .../elasticsearch/action/ActionModule.java | 4 + .../template/RenderSearchTemplateAction.java | 44 ++++++ .../template/RenderSearchTemplateRequest.java | 69 ++++++++ .../RenderSearchTemplateRequestBuilder.java | 42 +++++ .../RenderSearchTemplateResponse.java | 68 ++++++++ .../TransportRenderSearchTemplateAction.java | 66 ++++++++ .../java/org/elasticsearch/client/Client.java | 3 + .../client/IndicesAdminClient.java | 24 +++ .../client/support/AbstractClient.java | 19 +++ .../rest/action/RestActionModule.java | 3 + .../RestRenderSearchTemplateAction.java | 105 +++++++++++++ .../validate/RenderSearchTemplateTests.java | 147 ++++++++++++++++++ .../config/scripts/file_template_1.mustache | 1 + .../reference/search/search-template.asciidoc | 71 +++++++++ rest-api-spec/api/render_search_template.json | 19 +++ .../template/30_render_search_template.yaml | 110 +++++++++++++ 17 files changed, 795 insertions(+), 1 deletion(-) create mode 100644 core/src/main/java/org/elasticsearch/action/admin/indices/validate/template/RenderSearchTemplateAction.java create mode 100644 core/src/main/java/org/elasticsearch/action/admin/indices/validate/template/RenderSearchTemplateRequest.java create mode 100644 core/src/main/java/org/elasticsearch/action/admin/indices/validate/template/RenderSearchTemplateRequestBuilder.java create mode 100644 core/src/main/java/org/elasticsearch/action/admin/indices/validate/template/RenderSearchTemplateResponse.java create mode 100644 core/src/main/java/org/elasticsearch/action/admin/indices/validate/template/TransportRenderSearchTemplateAction.java create mode 100644 core/src/main/java/org/elasticsearch/rest/action/admin/indices/validate/template/RestRenderSearchTemplateAction.java create mode 100644 core/src/test/java/org/elasticsearch/validate/RenderSearchTemplateTests.java create mode 100644 core/src/test/resources/org/elasticsearch/validate/config/scripts/file_template_1.mustache create mode 100644 rest-api-spec/api/render_search_template.json create mode 100644 rest-api-spec/src/main/resources/rest-api-spec/test/template/30_render_search_template.yaml diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs index f4e75a7d41a20..5731b2f8244bf 100644 --- a/.settings/org.eclipse.core.resources.prefs +++ b/.settings/org.eclipse.core.resources.prefs @@ -1,7 +1,6 @@ eclipse.preferences.version=1 encoding//src/main/java=UTF-8 encoding//src/main/resources=UTF-8 -encoding//src/test/java=UTF-8 encoding//src/test/resources=UTF-8 encoding/=UTF-8 encoding/rest-api-spec=UTF-8 diff --git a/core/src/main/java/org/elasticsearch/action/ActionModule.java b/core/src/main/java/org/elasticsearch/action/ActionModule.java index ee48de4fc2342..2ceb9a976ea9a 100644 --- a/core/src/main/java/org/elasticsearch/action/ActionModule.java +++ b/core/src/main/java/org/elasticsearch/action/ActionModule.java @@ -21,6 +21,7 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; + import org.elasticsearch.action.admin.cluster.health.ClusterHealthAction; import org.elasticsearch.action.admin.cluster.health.TransportClusterHealthAction; import org.elasticsearch.action.admin.cluster.node.hotthreads.NodesHotThreadsAction; @@ -117,6 +118,8 @@ import org.elasticsearch.action.admin.indices.upgrade.post.UpgradeSettingsAction; import org.elasticsearch.action.admin.indices.validate.query.TransportValidateQueryAction; import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryAction; +import org.elasticsearch.action.admin.indices.validate.template.RenderSearchTemplateAction; +import org.elasticsearch.action.admin.indices.validate.template.TransportRenderSearchTemplateAction; import org.elasticsearch.action.admin.indices.warmer.delete.DeleteWarmerAction; import org.elasticsearch.action.admin.indices.warmer.delete.TransportDeleteWarmerAction; import org.elasticsearch.action.admin.indices.warmer.get.GetWarmersAction; @@ -302,6 +305,7 @@ protected void configure() { registerAction(ExplainAction.INSTANCE, TransportExplainAction.class); registerAction(ClearScrollAction.INSTANCE, TransportClearScrollAction.class); registerAction(RecoveryAction.INSTANCE, TransportRecoveryAction.class); + registerAction(RenderSearchTemplateAction.INSTANCE, TransportRenderSearchTemplateAction.class); //Indexed scripts registerAction(PutIndexedScriptAction.INSTANCE, TransportPutIndexedScriptAction.class); diff --git a/core/src/main/java/org/elasticsearch/action/admin/indices/validate/template/RenderSearchTemplateAction.java b/core/src/main/java/org/elasticsearch/action/admin/indices/validate/template/RenderSearchTemplateAction.java new file mode 100644 index 0000000000000..0cc71589576d4 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/action/admin/indices/validate/template/RenderSearchTemplateAction.java @@ -0,0 +1,44 @@ +/* + * 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.action.admin.indices.validate.template; + +import org.elasticsearch.action.Action; +import org.elasticsearch.client.ElasticsearchClient; + +public class RenderSearchTemplateAction extends Action { + + public static final RenderSearchTemplateAction INSTANCE = new RenderSearchTemplateAction(); + public static final String NAME = "indices:admin/render/template/search"; + + public RenderSearchTemplateAction() { + super(NAME); + } + + @Override + public RenderSearchTemplateRequestBuilder newRequestBuilder(ElasticsearchClient client) { + return new RenderSearchTemplateRequestBuilder(client, this); + } + + @Override + public RenderSearchTemplateResponse newResponse() { + return new RenderSearchTemplateResponse(); + } + +} diff --git a/core/src/main/java/org/elasticsearch/action/admin/indices/validate/template/RenderSearchTemplateRequest.java b/core/src/main/java/org/elasticsearch/action/admin/indices/validate/template/RenderSearchTemplateRequest.java new file mode 100644 index 0000000000000..bde255f2f7d40 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/action/admin/indices/validate/template/RenderSearchTemplateRequest.java @@ -0,0 +1,69 @@ +/* + * 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.action.admin.indices.validate.template; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.script.Template; + +import java.io.IOException; + +public class RenderSearchTemplateRequest extends ActionRequest { + + private Template template; + + public void template(Template template) { + this.template = template; + } + + public Template template() { + return template; + } + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException exception = null; + if (template == null) { + exception = new ActionRequestValidationException(); + exception.addValidationError("template must not be null"); + } + return exception; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + boolean hasTemplate = template!= null; + out.writeBoolean(hasTemplate); + if (hasTemplate) { + template.writeTo(out); + } + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + if (in.readBoolean()) { + template = Template.readTemplate(in); + } + } +} diff --git a/core/src/main/java/org/elasticsearch/action/admin/indices/validate/template/RenderSearchTemplateRequestBuilder.java b/core/src/main/java/org/elasticsearch/action/admin/indices/validate/template/RenderSearchTemplateRequestBuilder.java new file mode 100644 index 0000000000000..493dc7edc34d9 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/action/admin/indices/validate/template/RenderSearchTemplateRequestBuilder.java @@ -0,0 +1,42 @@ +/* + * 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.action.admin.indices.validate.template; + +import org.elasticsearch.action.ActionRequestBuilder; +import org.elasticsearch.client.ElasticsearchClient; +import org.elasticsearch.script.Template; + +public class RenderSearchTemplateRequestBuilder extends ActionRequestBuilder { + + public RenderSearchTemplateRequestBuilder(ElasticsearchClient client, + RenderSearchTemplateAction action) { + super(client, action, new RenderSearchTemplateRequest()); + } + + public RenderSearchTemplateRequestBuilder template(Template template) { + request.template(template); + return this; + } + + public Template template() { + return request.template(); + } + +} diff --git a/core/src/main/java/org/elasticsearch/action/admin/indices/validate/template/RenderSearchTemplateResponse.java b/core/src/main/java/org/elasticsearch/action/admin/indices/validate/template/RenderSearchTemplateResponse.java new file mode 100644 index 0000000000000..2d3ca018ed271 --- /dev/null +++ b/core/src/main/java/org/elasticsearch/action/admin/indices/validate/template/RenderSearchTemplateResponse.java @@ -0,0 +1,68 @@ +/* + * 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.action.admin.indices.validate.template; + +import org.elasticsearch.action.ActionResponse; +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.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; + +public class RenderSearchTemplateResponse extends ActionResponse implements ToXContent { + + private BytesReference source; + + public BytesReference source() { + return source; + } + + public void source(BytesReference source) { + this.source = source; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + boolean hasSource = source != null; + out.writeBoolean(hasSource); + if (hasSource) { + out.writeBytesReference(source); + } + } + + @Override + public void readFrom(StreamInput in) throws IOException { + super.readFrom(in); + if (in.readBoolean()) { + source = in.readBytesReference(); + } + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.rawField("template_output", source); + builder.endObject(); + return builder; + } +} diff --git a/core/src/main/java/org/elasticsearch/action/admin/indices/validate/template/TransportRenderSearchTemplateAction.java b/core/src/main/java/org/elasticsearch/action/admin/indices/validate/template/TransportRenderSearchTemplateAction.java new file mode 100644 index 0000000000000..c2ddcb13ae4bc --- /dev/null +++ b/core/src/main/java/org/elasticsearch/action/admin/indices/validate/template/TransportRenderSearchTemplateAction.java @@ -0,0 +1,66 @@ +/* + * 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.action.admin.indices.validate.template; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.AbstractRunnable; +import org.elasticsearch.script.ExecutableScript; +import org.elasticsearch.script.ScriptContext; +import org.elasticsearch.script.ScriptService; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; + +public class TransportRenderSearchTemplateAction extends HandledTransportAction { + + private final ScriptService scriptService; + + @Inject + protected TransportRenderSearchTemplateAction(ScriptService scriptService, Settings settings, ThreadPool threadPool, + TransportService transportService, ActionFilters actionFilters) { + super(settings, RenderSearchTemplateAction.NAME, threadPool, transportService, actionFilters, RenderSearchTemplateRequest.class); + this.scriptService = scriptService; + } + + @Override + protected void doExecute(final RenderSearchTemplateRequest request, final ActionListener listener) { + threadPool.generic().execute(new AbstractRunnable() { + + @Override + public void onFailure(Throwable t) { + listener.onFailure(t); + } + + @Override + protected void doRun() throws Exception { + ExecutableScript executable = scriptService.executable(request.template(), ScriptContext.Standard.SEARCH); + BytesReference processedTemplate = (BytesReference) executable.run(); + RenderSearchTemplateResponse response = new RenderSearchTemplateResponse(); + response.source(processedTemplate); + listener.onResponse(response); + } + }); + } + +} diff --git a/core/src/main/java/org/elasticsearch/client/Client.java b/core/src/main/java/org/elasticsearch/client/Client.java index 5c5714bbb6f7c..3f60f267cfbf7 100644 --- a/core/src/main/java/org/elasticsearch/client/Client.java +++ b/core/src/main/java/org/elasticsearch/client/Client.java @@ -21,6 +21,9 @@ import org.elasticsearch.action.ActionFuture; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.admin.indices.validate.template.RenderSearchTemplateRequest; +import org.elasticsearch.action.admin.indices.validate.template.RenderSearchTemplateRequestBuilder; +import org.elasticsearch.action.admin.indices.validate.template.RenderSearchTemplateResponse; import org.elasticsearch.action.bulk.BulkRequest; import org.elasticsearch.action.bulk.BulkRequestBuilder; import org.elasticsearch.action.bulk.BulkResponse; diff --git a/core/src/main/java/org/elasticsearch/client/IndicesAdminClient.java b/core/src/main/java/org/elasticsearch/client/IndicesAdminClient.java index 05bcc56711f48..7ab5078b04bdf 100644 --- a/core/src/main/java/org/elasticsearch/client/IndicesAdminClient.java +++ b/core/src/main/java/org/elasticsearch/client/IndicesAdminClient.java @@ -102,6 +102,9 @@ import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryRequest; import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryRequestBuilder; import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryResponse; +import org.elasticsearch.action.admin.indices.validate.template.RenderSearchTemplateRequest; +import org.elasticsearch.action.admin.indices.validate.template.RenderSearchTemplateRequestBuilder; +import org.elasticsearch.action.admin.indices.validate.template.RenderSearchTemplateResponse; import org.elasticsearch.action.admin.indices.warmer.delete.DeleteWarmerRequest; import org.elasticsearch.action.admin.indices.warmer.delete.DeleteWarmerRequestBuilder; import org.elasticsearch.action.admin.indices.warmer.delete.DeleteWarmerResponse; @@ -717,6 +720,27 @@ public interface IndicesAdminClient extends ElasticsearchClient { */ ValidateQueryRequestBuilder prepareValidateQuery(String... indices); + /** + * Return the rendered search request for a given search template. + * + * @param request The request + * @return The result future + */ + ActionFuture renderSearchTemplate(RenderSearchTemplateRequest request); + + /** + * Return the rendered search request for a given search template. + * + * @param request The request + * @param listener A listener to be notified of the result + */ + void renderSearchTemplate(RenderSearchTemplateRequest request, ActionListener listener); + + /** + * Return the rendered search request for a given search template. + */ + RenderSearchTemplateRequestBuilder prepareRenderSearchTemplate(); + /** * Puts an index search warmer to be applies when applicable. */ diff --git a/core/src/main/java/org/elasticsearch/client/support/AbstractClient.java b/core/src/main/java/org/elasticsearch/client/support/AbstractClient.java index 761acd011d1f4..a4d271dc59913 100644 --- a/core/src/main/java/org/elasticsearch/client/support/AbstractClient.java +++ b/core/src/main/java/org/elasticsearch/client/support/AbstractClient.java @@ -204,6 +204,10 @@ import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryRequest; import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryRequestBuilder; import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryResponse; +import org.elasticsearch.action.admin.indices.validate.template.RenderSearchTemplateAction; +import org.elasticsearch.action.admin.indices.validate.template.RenderSearchTemplateRequest; +import org.elasticsearch.action.admin.indices.validate.template.RenderSearchTemplateRequestBuilder; +import org.elasticsearch.action.admin.indices.validate.template.RenderSearchTemplateResponse; import org.elasticsearch.action.admin.indices.warmer.delete.DeleteWarmerAction; import org.elasticsearch.action.admin.indices.warmer.delete.DeleteWarmerRequest; import org.elasticsearch.action.admin.indices.warmer.delete.DeleteWarmerRequestBuilder; @@ -1594,6 +1598,21 @@ public ValidateQueryRequestBuilder prepareValidateQuery(String... indices) { return new ValidateQueryRequestBuilder(this, ValidateQueryAction.INSTANCE).setIndices(indices); } + @Override + public ActionFuture renderSearchTemplate(final RenderSearchTemplateRequest request) { + return execute(RenderSearchTemplateAction.INSTANCE, request); + } + + @Override + public void renderSearchTemplate(final RenderSearchTemplateRequest request, final ActionListener listener) { + execute(RenderSearchTemplateAction.INSTANCE, request, listener); + } + + @Override + public RenderSearchTemplateRequestBuilder prepareRenderSearchTemplate() { + return new RenderSearchTemplateRequestBuilder(this, RenderSearchTemplateAction.INSTANCE); + } + @Override public ActionFuture putWarmer(PutWarmerRequest request) { return execute(PutWarmerAction.INSTANCE, request); diff --git a/core/src/main/java/org/elasticsearch/rest/action/RestActionModule.java b/core/src/main/java/org/elasticsearch/rest/action/RestActionModule.java index a2d09f4f80e1e..579c42a859447 100644 --- a/core/src/main/java/org/elasticsearch/rest/action/RestActionModule.java +++ b/core/src/main/java/org/elasticsearch/rest/action/RestActionModule.java @@ -20,6 +20,7 @@ package org.elasticsearch.rest.action; import com.google.common.collect.Lists; + import org.elasticsearch.common.inject.AbstractModule; import org.elasticsearch.common.inject.multibindings.Multibinder; import org.elasticsearch.rest.BaseRestHandler; @@ -76,6 +77,7 @@ import org.elasticsearch.rest.action.admin.indices.template.put.RestPutIndexTemplateAction; import org.elasticsearch.rest.action.admin.indices.upgrade.RestUpgradeAction; import org.elasticsearch.rest.action.admin.indices.validate.query.RestValidateQueryAction; +import org.elasticsearch.rest.action.admin.indices.validate.template.RestRenderSearchTemplateAction; import org.elasticsearch.rest.action.admin.indices.warmer.delete.RestDeleteWarmerAction; import org.elasticsearch.rest.action.admin.indices.warmer.get.RestGetWarmerAction; import org.elasticsearch.rest.action.admin.indices.warmer.put.RestPutWarmerAction; @@ -207,6 +209,7 @@ protected void configure() { bind(RestSearchScrollAction.class).asEagerSingleton(); bind(RestClearScrollAction.class).asEagerSingleton(); bind(RestMultiSearchAction.class).asEagerSingleton(); + bind(RestRenderSearchTemplateAction.class).asEagerSingleton(); bind(RestValidateQueryAction.class).asEagerSingleton(); diff --git a/core/src/main/java/org/elasticsearch/rest/action/admin/indices/validate/template/RestRenderSearchTemplateAction.java b/core/src/main/java/org/elasticsearch/rest/action/admin/indices/validate/template/RestRenderSearchTemplateAction.java new file mode 100644 index 0000000000000..2a931b537614b --- /dev/null +++ b/core/src/main/java/org/elasticsearch/rest/action/admin/indices/validate/template/RestRenderSearchTemplateAction.java @@ -0,0 +1,105 @@ +/* + * 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.rest.action.admin.indices.validate.template; + +import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.action.admin.indices.validate.template.RenderSearchTemplateRequest; +import org.elasticsearch.action.admin.indices.validate.template.RenderSearchTemplateResponse; +import org.elasticsearch.client.Client; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; +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.rest.BaseRestHandler; +import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestChannel; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestResponse; +import org.elasticsearch.rest.action.support.RestActions; +import org.elasticsearch.rest.action.support.RestBuilderListener; +import org.elasticsearch.script.Script.ScriptField; +import org.elasticsearch.script.ScriptService.ScriptType; +import org.elasticsearch.script.Template; +import org.elasticsearch.script.mustache.MustacheScriptEngineService; + +import java.util.Map; + +import static org.elasticsearch.rest.RestRequest.Method.GET; +import static org.elasticsearch.rest.RestRequest.Method.POST; +import static org.elasticsearch.rest.RestStatus.OK; + +public class RestRenderSearchTemplateAction extends BaseRestHandler { + + @Inject + public RestRenderSearchTemplateAction(Settings settings, RestController controller, Client client) { + super(settings, controller, client); + controller.registerHandler(GET, "/_render/template", this); + controller.registerHandler(POST, "/_render/template", this); + controller.registerHandler(GET, "/_render/template/{id}", this); + controller.registerHandler(POST, "/_render/template/{id}", this); + } + + @Override + protected void handleRequest(RestRequest request, RestChannel channel, Client client) throws Exception { + RenderSearchTemplateRequest renderSearchTemplateRequest; + BytesReference source = RestActions.getRestContent(request); + XContentParser parser = XContentFactory.xContent(source).createParser(source); + String templateId = request.param("id"); + final Template template; + if (templateId == null) { + template = Template.parse(parser); + } else { + Map params = null; + String currentFieldName = null; + XContentParser.Token token = parser.nextToken(); + if (token != XContentParser.Token.START_OBJECT) { + throw new ElasticsearchParseException("request body must start with [" + XContentParser.Token.START_OBJECT + "] but found [" + token + "]"); + } + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (ScriptField.PARAMS.match(currentFieldName)) { + if (token == XContentParser.Token.START_OBJECT) { + params = parser.map(); + } else { + throw new ElasticsearchParseException("Expected [" + XContentParser.Token.START_OBJECT + "] for [params] but found [" + token + "]"); + } + } else { + throw new ElasticsearchParseException("Unknown field [" + currentFieldName + "] of type [" + token + "]"); + } + } + template = new Template(templateId, ScriptType.INDEXED, MustacheScriptEngineService.NAME, null, params); + } + renderSearchTemplateRequest = new RenderSearchTemplateRequest(); + renderSearchTemplateRequest.template(template); + client.admin().indices().renderSearchTemplate(renderSearchTemplateRequest, new RestBuilderListener(channel) { + + @Override + public RestResponse buildResponse(RenderSearchTemplateResponse response, XContentBuilder builder) throws Exception { + builder.prettyPrint(); + response.toXContent(builder, ToXContent.EMPTY_PARAMS); + return new BytesRestResponse(OK, builder); + }}); + } +} diff --git a/core/src/test/java/org/elasticsearch/validate/RenderSearchTemplateTests.java b/core/src/test/java/org/elasticsearch/validate/RenderSearchTemplateTests.java new file mode 100644 index 0000000000000..9ed0e59206b45 --- /dev/null +++ b/core/src/test/java/org/elasticsearch/validate/RenderSearchTemplateTests.java @@ -0,0 +1,147 @@ +/* + * 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.validate; + +import org.elasticsearch.action.admin.indices.validate.template.RenderSearchTemplateResponse; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.script.ScriptService.ScriptType; +import org.elasticsearch.script.Template; +import org.elasticsearch.script.mustache.MustacheScriptEngineService; +import org.elasticsearch.test.ElasticsearchIntegrationTest; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.elasticsearch.common.settings.Settings.settingsBuilder; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; + +@ElasticsearchIntegrationTest.SuiteScopeTest +public class RenderSearchTemplateTests extends ElasticsearchIntegrationTest { + + private static final String TEMPLATE_CONTENTS = "{\"size\":\"{{size}}\",\"query\":{\"match\":{\"foo\":\"{{value}}\"}},\"aggs\":{\"objects\":{\"terms\":{\"field\":\"{{value}}\",\"size\":\"{{size}}\"}}}}"; + + @Override + protected void setupSuiteScopeCluster() throws Exception { + client().preparePutIndexedScript(MustacheScriptEngineService.NAME, "index_template_1", "{ \"template\": " + TEMPLATE_CONTENTS + " }").get(); + } + + @Override + public Settings nodeSettings(int nodeOrdinal) { + //Set path so ScriptService will pick up the test scripts + return settingsBuilder().put(super.nodeSettings(nodeOrdinal)) + .put("path.conf", this.getDataPath("config")).build(); + } + + @Test + public void inlineTemplate() { + Map params = new HashMap<>(); + params.put("value", "bar"); + params.put("size", 20); + Template template = new Template(TEMPLATE_CONTENTS, ScriptType.INLINE, MustacheScriptEngineService.NAME, XContentType.JSON, params); + RenderSearchTemplateResponse response = client().admin().indices().prepareRenderSearchTemplate().template(template).get(); + assertThat(response, notNullValue()); + BytesReference source = response.source(); + assertThat(source, notNullValue()); + Map sourceAsMap = XContentHelper.convertToMap(source, false).v2(); + assertThat(sourceAsMap, notNullValue()); + String expected = TEMPLATE_CONTENTS.replace("{{value}}", "bar").replace("{{size}}", "20"); + Map expectedMap = XContentHelper.convertToMap(new BytesArray(expected), false).v2(); + assertThat(sourceAsMap, equalTo(expectedMap)); + + params = new HashMap<>(); + params.put("value", "baz"); + params.put("size", 100); + template = new Template(TEMPLATE_CONTENTS, ScriptType.INLINE, MustacheScriptEngineService.NAME, XContentType.JSON, params); + response = client().admin().indices().prepareRenderSearchTemplate().template(template).get(); + assertThat(response, notNullValue()); + source = response.source(); + assertThat(source, notNullValue()); + sourceAsMap = XContentHelper.convertToMap(source, false).v2(); + expected = TEMPLATE_CONTENTS.replace("{{value}}", "baz").replace("{{size}}", "100"); + expectedMap = XContentHelper.convertToMap(new BytesArray(expected), false).v2(); + assertThat(sourceAsMap, equalTo(expectedMap)); + } + + @Test + public void indexedTemplate() { + Map params = new HashMap<>(); + params.put("value", "bar"); + params.put("size", 20); + Template template = new Template("index_template_1", ScriptType.INDEXED, MustacheScriptEngineService.NAME, XContentType.JSON, params); + RenderSearchTemplateResponse response = client().admin().indices().prepareRenderSearchTemplate().template(template).get(); + assertThat(response, notNullValue()); + BytesReference source = response.source(); + assertThat(source, notNullValue()); + Map sourceAsMap = XContentHelper.convertToMap(source, false).v2(); + assertThat(sourceAsMap, notNullValue()); + String expected = TEMPLATE_CONTENTS.replace("{{value}}", "bar").replace("{{size}}", "20"); + Map expectedMap = XContentHelper.convertToMap(new BytesArray(expected), false).v2(); + assertThat(sourceAsMap, equalTo(expectedMap)); + + params = new HashMap<>(); + params.put("value", "baz"); + params.put("size", 100); + template = new Template("index_template_1", ScriptType.INDEXED, MustacheScriptEngineService.NAME, XContentType.JSON, params); + response = client().admin().indices().prepareRenderSearchTemplate().template(template).get(); + assertThat(response, notNullValue()); + source = response.source(); + assertThat(source, notNullValue()); + sourceAsMap = XContentHelper.convertToMap(source, false).v2(); + expected = TEMPLATE_CONTENTS.replace("{{value}}", "baz").replace("{{size}}", "100"); + expectedMap = XContentHelper.convertToMap(new BytesArray(expected), false).v2(); + assertThat(sourceAsMap, equalTo(expectedMap)); + } + + @Test + public void fileTemplate() { + Map params = new HashMap<>(); + params.put("value", "bar"); + params.put("size", 20); + Template template = new Template("file_template_1", ScriptType.FILE, MustacheScriptEngineService.NAME, XContentType.JSON, params); + RenderSearchTemplateResponse response = client().admin().indices().prepareRenderSearchTemplate().template(template).get(); + assertThat(response, notNullValue()); + BytesReference source = response.source(); + assertThat(source, notNullValue()); + Map sourceAsMap = XContentHelper.convertToMap(source, false).v2(); + assertThat(sourceAsMap, notNullValue()); + String expected = TEMPLATE_CONTENTS.replace("{{value}}", "bar").replace("{{size}}", "20"); + Map expectedMap = XContentHelper.convertToMap(new BytesArray(expected), false).v2(); + assertThat(sourceAsMap, equalTo(expectedMap)); + + params = new HashMap<>(); + params.put("value", "baz"); + params.put("size", 100); + template = new Template("file_template_1", ScriptType.FILE, MustacheScriptEngineService.NAME, XContentType.JSON, params); + response = client().admin().indices().prepareRenderSearchTemplate().template(template).get(); + assertThat(response, notNullValue()); + source = response.source(); + assertThat(source, notNullValue()); + sourceAsMap = XContentHelper.convertToMap(source, false).v2(); + expected = TEMPLATE_CONTENTS.replace("{{value}}", "baz").replace("{{size}}", "100"); + expectedMap = XContentHelper.convertToMap(new BytesArray(expected), false).v2(); + assertThat(sourceAsMap, equalTo(expectedMap)); + } +} diff --git a/core/src/test/resources/org/elasticsearch/validate/config/scripts/file_template_1.mustache b/core/src/test/resources/org/elasticsearch/validate/config/scripts/file_template_1.mustache new file mode 100644 index 0000000000000..969dc8d598797 --- /dev/null +++ b/core/src/test/resources/org/elasticsearch/validate/config/scripts/file_template_1.mustache @@ -0,0 +1 @@ +{"size":"{{size}}","query":{"match":{"foo":"{{value}}"}},"aggs":{"objects":{"terms":{"field":"{{value}}","size":"{{size}}"}}}} \ No newline at end of file diff --git a/docs/reference/search/search-template.asciidoc b/docs/reference/search/search-template.asciidoc index b92dbfaa79516..1b95f376f8757 100644 --- a/docs/reference/search/search-template.asciidoc +++ b/docs/reference/search/search-template.asciidoc @@ -298,3 +298,74 @@ GET /_search/template } ------------------------------------------ <1> Name of the the query template stored in the `.scripts` index. + +[float] +==== Validating templates + +A template can be rendered in a response with given parameters using + +[source,js] +------------------------------------------ +GET /_render/template +{ + "inline": { + "query": { + "terms": { + "status": [ + "{{#status}}", + "{{.}}", + "{{/status}}" + ] + } + } + }, + "params": { + "status": [ "pending", "published" ] + } +} +------------------------------------------ + +This call will return the rendered template: + +[source,js] +------------------------------------------ +{ + "template_output": { + "query": { + "terms": { + "status": [ <1> + "pending", + "published" + ] + } + } + } +} +------------------------------------------ +<1> `status` array has been populated with values from the `params` object. + +File and indexed templates can also be rendered by replacing `inline` with +`file` or `id` respectively. For example, to render a file template + +[source,js] +------------------------------------------ +GET /_render/template +{ + "file": "my_template", + "params": { + "status": [ "pending", "published" ] + } +} +------------------------------------------ + +Pre-registered templates can also be rendered using + +[source,js] +------------------------------------------ +GET /_render/template/ +{ + "params": { + "... + } +} +------------------------------------------ diff --git a/rest-api-spec/api/render_search_template.json b/rest-api-spec/api/render_search_template.json new file mode 100644 index 0000000000000..8f27c12b819da --- /dev/null +++ b/rest-api-spec/api/render_search_template.json @@ -0,0 +1,19 @@ +{ + "render_search_template": { + "documentation": "http://www.elasticsearch.org/guide/en/elasticsearch/reference/master/search-template.html", + "methods": ["GET", "POST"], + "url": { + "path": "/_render/template", + "paths": [ "/_render/template", "/_render/template/{id}" ], + "parts": { + "id": { + "type" : "string", + "description" : "The id of the stored search template" + } + } + }, + "body": { + "description": "The search definition template and its params" + } + } +} diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/template/30_render_search_template.yaml b/rest-api-spec/src/main/resources/rest-api-spec/test/template/30_render_search_template.yaml new file mode 100644 index 0000000000000..5d5c3d5f7616e --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/template/30_render_search_template.yaml @@ -0,0 +1,110 @@ +--- +"Indexed Template validate tests": + + - do: + put_template: + id: "1" + body: { "template": { "query": { "match": { "text": "{{my_value}}" } }, "aggs": { "my_terms": { "terms": { "field": "{{my_field}}" } } } } } + - match: { _id: "1" } + + - do: + indices.refresh: {} + + - do: + render_search_template: + body: { "id": "1", "params": { "my_value": "foo", "my_field": "field1" } } + + - match: { template_output.query.match.text: "foo" } + - match: { template_output.aggs.my_terms.terms.field: "field1" } + + - do: + render_search_template: + body: { "id": "1", "params": { "my_value": "bar", "my_field": "my_other_field" } } + + - match: { template_output.query.match.text: "bar" } + - match: { template_output.aggs.my_terms.terms.field: "my_other_field" } + + - do: + render_search_template: + id: "1" + body: { "params": { "my_value": "bar", "my_field": "field1" } } + + - match: { template_output.query.match.text: "bar" } + - match: { template_output.aggs.my_terms.terms.field: "field1" } + +--- +"Inline Template validate tests": + + - do: + render_search_template: + body: { "inline": { "query": { "match": { "text": "{{my_value}}" } }, "aggs": { "my_terms": { "terms": { "field": "{{my_field}}" } } } }, "params": { "my_value": "foo", "my_field": "field1" } } + + - match: { template_output.query.match.text: "foo" } + - match: { template_output.aggs.my_terms.terms.field: "field1" } + + - do: + render_search_template: + body: { "inline": { "query": { "match": { "text": "{{my_value}}" } }, "aggs": { "my_terms": { "terms": { "field": "{{my_field}}" } } } }, "params": { "my_value": "bar", "my_field": "my_other_field" } } + + - match: { template_output.query.match.text: "bar" } + - match: { template_output.aggs.my_terms.terms.field: "my_other_field" } + + - do: + catch: /Improperly.closed.variable.in.query-template/ + render_search_template: + body: { "inline": { "query": { "match": { "text": "{{{my_value}}" } }, "aggs": { "my_terms": { "terms": { "field": "{{my_field}}" } } } }, "params": { "my_value": "bar", "my_field": "field1" } } +--- +"Escaped Indexed Template validate tests": + + - do: + put_template: + id: "1" + body: { "template": "{ \"query\": { \"match\": { \"text\": \"{{my_value}}\" } }, \"size\": {{my_size}} }" } + - match: { _id: "1" } + + - do: + indices.refresh: {} + + - do: + render_search_template: + body: { "id": "1", "params": { "my_value": "foo", "my_size": 20 } } + + - match: { template_output.query.match.text: "foo" } + - match: { template_output.size: 20 } + + - do: + render_search_template: + body: { "id": "1", "params": { "my_value": "bar", "my_size": 100 } } + + - match: { template_output.query.match.text: "bar" } + - match: { template_output.size: 100 } + + - do: + render_search_template: + id: "1" + body: { "params": { "my_value": "bar", "my_size": 100 } } + + - match: { template_output.query.match.text: "bar" } + - match: { template_output.size: 100 } + +--- +"Escaped Inline Template validate tests": + + - do: + render_search_template: + body: { "inline": "{ \"query\": { \"match\": { \"text\": \"{{my_value}}\" } }, \"size\": {{my_size}} }", "params": { "my_value": "foo", "my_size": 20 } } + + - match: { template_output.query.match.text: "foo" } + - match: { template_output.size: 20 } + + - do: + render_search_template: + body: { "inline": "{ \"query\": { \"match\": { \"text\": \"{{my_value}}\" } }, \"size\": {{my_size}} }", "params": { "my_value": "bar", "my_size": 100 } } + + - match: { template_output.query.match.text: "bar" } + - match: { template_output.size: 100 } + + - do: + catch: /Improperly.closed.variable.in.query-template/ + render_search_template: + body: { "inline": "{ \"query\": { \"match\": { \"text\": \"{{{my_value}}\" } }, \"size\": {{my_size}} }", "params": { "my_value": "bar", "my_size": 100 } }