diff --git a/docs/painless/painless-api-reference/painless-api-reference-shared/index.asciidoc b/docs/painless/painless-api-reference/painless-api-reference-shared/index.asciidoc index c8bbedadf6b4e..1785ab787901c 100644 --- a/docs/painless/painless-api-reference/painless-api-reference-shared/index.asciidoc +++ b/docs/painless/painless-api-reference/painless-api-reference-shared/index.asciidoc @@ -420,6 +420,7 @@ The following classes are available grouped by their respective packages. Click <> * <> +* <> ==== org.elasticsearch.script <> diff --git a/docs/painless/painless-api-reference/painless-api-reference-shared/packages.asciidoc b/docs/painless/painless-api-reference/painless-api-reference-shared/packages.asciidoc index 584d7ade9ec7c..f86a2b3e7b1e2 100644 --- a/docs/painless/painless-api-reference/painless-api-reference-shared/packages.asciidoc +++ b/docs/painless/painless-api-reference/painless-api-reference-shared/packages.asciidoc @@ -8509,6 +8509,12 @@ See the <> for a high-level overview === Shared API for package org.elasticsearch.script See the <> for a high-level overview of all packages and classes. +[[painless-api-reference-shared-Json]] +==== Json +* static def load(String) +* static String dump(Object) +* static String dump(Object, boolean) + [[painless-api-reference-shared-JodaCompatibleZonedDateTime]] ==== JodaCompatibleZonedDateTime * int {java11-javadoc}/java.base/java/time/chrono/ChronoZonedDateTime.html#compareTo(java.time.chrono.ChronoZonedDateTime)[compareTo](ChronoZonedDateTime) diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/api/Json.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/api/Json.java new file mode 100644 index 0000000000000..5ab6f085fe4da --- /dev/null +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/api/Json.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.painless.api; + +import org.elasticsearch.common.xcontent.DeprecationHandler; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.json.JsonXContent; + +import java.io.IOException; + +public class Json { + public static Object load(String json) throws IOException{ + XContentParser parser = JsonXContent.jsonXContent.createParser( + NamedXContentRegistry.EMPTY, + DeprecationHandler.THROW_UNSUPPORTED_OPERATION, + json); + + switch (parser.nextToken()) { + case START_ARRAY: + return parser.list(); + case START_OBJECT: + return parser.map(); + case VALUE_NUMBER: + return parser.numberValue(); + case VALUE_BOOLEAN: + return parser.booleanValue(); + case VALUE_STRING: + return parser.text(); + default: + return null; + } + } + + public static String dump(Object data) throws IOException { + return dump(data, false); + } + + public static String dump(Object data, boolean pretty) throws IOException { + XContentBuilder builder = JsonXContent.contentBuilder(); + if (pretty) { + builder.prettyPrint(); + } + builder.value(data); + builder.flush(); + return builder.getOutputStream().toString(); + } +} diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/JsonTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/JsonTests.java new file mode 100644 index 0000000000000..a9ab7fe4d7425 --- /dev/null +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/JsonTests.java @@ -0,0 +1,49 @@ +/* + * 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.painless; + +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; + +public class JsonTests extends ScriptTestCase { + public void testDump() { + // simple object dump + Object output = exec("Json.dump(params.data)", singletonMap("data", singletonMap("hello", "world")), true); + assertEquals("{\"hello\":\"world\"}", output); + + output = exec("Json.dump(params.data)", singletonMap("data", singletonList(42)), true); + assertEquals("[42]", output); + + // pretty print + output = exec("Json.dump(params.data, true)", singletonMap("data", singletonMap("hello", "world")), true); + assertEquals("{\n \"hello\" : \"world\"\n}", output); + } + + public void testLoad() { + String json = "{\"hello\":\"world\"}"; + Object output = exec("Json.load(params.json)", singletonMap("json", json), true); + assertEquals(singletonMap("hello", "world"), output); + + json = "[42]"; + output = exec("Json.load(params.json)", singletonMap("json", json), true); + assertEquals(singletonList(42), output); + } + +} diff --git a/x-pack/plugin/watcher/build.gradle b/x-pack/plugin/watcher/build.gradle index b890f0053209c..c9936a6ad2c94 100644 --- a/x-pack/plugin/watcher/build.gradle +++ b/x-pack/plugin/watcher/build.gradle @@ -5,7 +5,7 @@ esplugin { classname 'org.elasticsearch.xpack.watcher.Watcher' hasNativeController false requiresKeystore false - extendedPlugins = ['x-pack-core'] + extendedPlugins = ['x-pack-core', 'lang-painless'] } archivesBaseName = 'x-pack-watcher' @@ -21,6 +21,7 @@ tasks.named("dependencyLicenses").configure { dependencies { compileOnly project(':server') + compileOnly project(':modules:lang-painless:spi') compileOnly project(path: xpackModule('core'), configuration: 'default') compileOnly project(path: ':modules:transport-netty4') compileOnly project(path: ':plugins:transport-nio') diff --git a/x-pack/plugin/watcher/qa/rest/src/test/resources/rest-api-spec/test/watcher/execute_watch/90_painless_context.yml b/x-pack/plugin/watcher/qa/rest/src/test/resources/rest-api-spec/test/watcher/execute_watch/90_painless_context.yml new file mode 100644 index 0000000000000..4716b5801bc30 --- /dev/null +++ b/x-pack/plugin/watcher/qa/rest/src/test/resources/rest-api-spec/test/watcher/execute_watch/90_painless_context.yml @@ -0,0 +1,55 @@ +--- +"Test for json (de)serialization in painless' watcher context": + - do: + watcher.execute_watch: + body: > + { + "watch": { + "trigger": { + "schedule" : { "cron" : "0 0 0 1 * ? 2099" } + }, + "input": { + "simple": { + "foo": "bar" + } + }, + "condition": { + "script": { + "source": "Json.dump([1, 2, 3, 4]) == '[1,2,3,4]'" + } + }, + "transform": { + "script": { + "source": "return Json.load('{\"hello\": \"world\"}')" + } + }, + "actions": { + "indexme" : { + "index" : { + "index" : "my_test_index", + "doc_id": "my-id" + } + } + } + } + } + + - match: { watch_record.trigger_event.type: "manual" } + - match: { watch_record.state: "executed" } + - match: { watch_record.status.execution_state: "executed" } + - match: { watch_record.status.state.active: true } + - is_true: watch_record.node + - match: { watch_record.status.actions.indexme.ack.state: "ackable" } + - gt: { watch_record.result.execution_duration: 0 } + + - do: + indices.refresh: {} + + - do: + get: + index: my_test_index + id: my-id + + - match: { found: true } + - match: { _source.hello: world } + diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/Watcher.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/Watcher.java index ab16e28207281..c0a50d923ab27 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/Watcher.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/Watcher.java @@ -178,6 +178,7 @@ import java.io.IOException; import java.io.UncheckedIOException; import java.time.Clock; +import java.time.Duration; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.ArrayList; diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherPainlessExtension.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherPainlessExtension.java new file mode 100644 index 0000000000000..c055c0e9e1a74 --- /dev/null +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/WatcherPainlessExtension.java @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.watcher; + +import org.elasticsearch.painless.spi.PainlessExtension; +import org.elasticsearch.painless.spi.Whitelist; +import org.elasticsearch.painless.spi.WhitelistLoader; +import org.elasticsearch.script.ScriptContext; +import org.elasticsearch.xpack.watcher.condition.WatcherConditionScript; +import org.elasticsearch.xpack.watcher.transform.script.WatcherTransformScript; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class WatcherPainlessExtension implements PainlessExtension { + + private static final Whitelist WHITELIST = + WhitelistLoader.loadFromResourceFiles(WatcherPainlessExtension.class, "painless_whitelist.txt"); + + @Override + public Map, List> getContextWhitelists() { + Map, List> contextWhiltelists = new HashMap<>(); + contextWhiltelists.put(WatcherConditionScript.CONTEXT, Collections.singletonList(WHITELIST)); + contextWhiltelists.put(WatcherTransformScript.CONTEXT, Collections.singletonList(WHITELIST)); + return Collections.unmodifiableMap(contextWhiltelists); + } +} diff --git a/x-pack/plugin/watcher/src/main/resources/META-INF/services/org.elasticsearch.painless.spi.PainlessExtension b/x-pack/plugin/watcher/src/main/resources/META-INF/services/org.elasticsearch.painless.spi.PainlessExtension new file mode 100644 index 0000000000000..47503853172d4 --- /dev/null +++ b/x-pack/plugin/watcher/src/main/resources/META-INF/services/org.elasticsearch.painless.spi.PainlessExtension @@ -0,0 +1 @@ +org.elasticsearch.xpack.watcher.WatcherPainlessExtension diff --git a/x-pack/plugin/watcher/src/main/resources/org/elasticsearch/xpack/watcher/painless_whitelist.txt b/x-pack/plugin/watcher/src/main/resources/org/elasticsearch/xpack/watcher/painless_whitelist.txt new file mode 100644 index 0000000000000..d69801e593217 --- /dev/null +++ b/x-pack/plugin/watcher/src/main/resources/org/elasticsearch/xpack/watcher/painless_whitelist.txt @@ -0,0 +1,15 @@ +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# + +class org.elasticsearch.painless.api.Json { + def load(String) + String dump(def) + String dump(def, boolean) +} + +static_import { + def load(String) from_class org.elasticsearch.painless.api.Json +}