diff --git a/docs/changelog/121914.yaml b/docs/changelog/121914.yaml deleted file mode 100644 index 33e5f1351236e..0000000000000 --- a/docs/changelog/121914.yaml +++ /dev/null @@ -1,5 +0,0 @@ -pr: 121914 -summary: Support Fields API in conditional ingest processors -area: Infra/Core -type: feature -issues: [] diff --git a/modules/ingest-common/src/yamlRestTest/resources/rest-api-spec/test/ingest/210_conditional_processor.yml b/modules/ingest-common/src/yamlRestTest/resources/rest-api-spec/test/ingest/210_conditional_processor.yml index 0ba1a0f7d78d7..8ad2be2b41fe4 100644 --- a/modules/ingest-common/src/yamlRestTest/resources/rest-api-spec/test/ingest/210_conditional_processor.yml +++ b/modules/ingest-common/src/yamlRestTest/resources/rest-api-spec/test/ingest/210_conditional_processor.yml @@ -74,150 +74,3 @@ teardown: - match: { _source.bytes_source_field: "1kb" } - match: { _source.conditional_field: "bar" } - is_false: _source.bytes_target_field - ---- -"Test conditional processor with fields API": - - do: - ingest.put_pipeline: - id: "my_pipeline" - body: - description: "_description" - processors: - - set: - if: "field('get.field').get('') == 'one'" - field: "one" - value: 1 - - set: - if: "field('get.field').get('') == 'two'" - field: "missing" - value: "missing" - - set: - if: " /* avoid yaml stash */ $('get.field', 'one') == 'one'" - field: "dollar" - value: true - - set: - if: "field('missing.field').get('fallback') == 'fallback'" - field: "fallback" - value: "fallback" - - set: - if: "field('nested.array.get.with.index.field').get(1, null) == 'two'" - field: "two" - value: 2 - - set: - if: "field('getName.field').getName() == 'getName.field'" - field: "three" - value: 3 - - set: - if: "field('existing.field').exists()" - field: "four" - value: 4 - - set: - if: "!field('empty.field').isEmpty()" - field: "missing" - value: "missing" - - set: - if: "field('size.field').size() == 2" - field: "five" - value: 5 - - set: - if: > - def iterator = field('iterator.field').iterator(); - def sum = 0; - while (iterator.hasNext()) { - sum += iterator.next(); - } - return sum == 6; - field: "six" - value: 6 - - set: - if: "field('hasValue.field').hasValue(v -> v == 'two')" - field: "seven" - value: 7 - - match: { acknowledged: true } - - - do: - index: - index: test - id: "1" - pipeline: "my_pipeline" - body: - get.field: "one" - nested: - array: - get.with.index.field: ["one", "two", "three"] - getName.field: "my_name" - existing.field: "indeed" - empty.field: [] - size.field: ["one", "two"] - iterator.field: [1, 2, 3] - hasValue.field: ["one", "two", "three"] - - - do: - get: - index: test - id: "1" - - match: { _source.get\.field: "one" } - - match: { _source.one: 1 } - - is_false: _source.missing - - is_true: _source.dollar - - match: { _source.fallback: "fallback" } - - match: { _source.nested.array.get\.with\.index\.field: ["one", "two", "three"] } - - match: { _source.two: 2 } - - match: { _source.three: 3 } - - match: { _source.four: 4 } - - match: { _source.five: 5 } - - match: { _source.six: 6 } - - match: { _source.seven: 7 } - ---- -"Test fields iterator is unmodifiable": - - do: - ingest.put_pipeline: - id: "my_pipeline" - body: - description: "_description" - processors: - - set: - if: > - def iterator = field('iterator.field').iterator(); - def sum = 0; - while (iterator.hasNext()) { - sum += iterator.next(); - iterator.remove(); - } - return sum == 6; - field: "sum" - value: 6 - - match: { acknowledged: true } - - - do: - index: - index: test - id: "1" - pipeline: "my_pipeline" - body: - test.field: [1, 2, 3] - - match: { error: null } - - - do: - index: - index: test - id: "2" - pipeline: "my_pipeline" - body: - iterator.field: [1, 2, 3] - catch: bad_request - - length: { error.root_cause: 1 } - - - do: - get: - index: test - id: "1" - - match: { _source.test\.field: [1, 2, 3] } - - is_false: _source.sum - - - do: - get: - index: test - id: "2" - catch: missing diff --git a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.ingest.txt b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.ingest.txt index 5031682e5a123..9cc88ab4f4607 100644 --- a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.ingest.txt +++ b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.ingest.txt @@ -46,12 +46,15 @@ class org.elasticsearch.script.IngestScript { } class org.elasticsearch.script.field.WriteField { + String getName() boolean exists() WriteField move(def) WriteField overwrite(def) void remove() WriteField set(def) WriteField append(def) + boolean isEmpty() + int size() Iterator iterator() def get(def) def get(int, def) diff --git a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.processor_conditional.txt b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.processor_conditional.txt deleted file mode 100644 index eef74fb1be36b..0000000000000 --- a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.processor_conditional.txt +++ /dev/null @@ -1,22 +0,0 @@ -# - # Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - # or more contributor license agreements. Licensed under the "Elastic License - # 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - # Public License v 1"; you may not use this file except in compliance with, at - # your election, the "Elastic License 2.0", the "GNU Affero General Public - # License v3.0 only", or the "Server Side Public License, v 1". -# - -# This file contains a whitelist for conditional ingest scripts - -class org.elasticsearch.script.IngestConditionalScript { - SourceMapField field(String) -} - -class org.elasticsearch.script.field.SourceMapField { - boolean exists() - Iterator iterator() - def get(def) - def get(int, def) - boolean hasValue(Predicate) -} diff --git a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.reindex.txt b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.reindex.txt index c75586137b283..fa23e4b6acd1a 100644 --- a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.reindex.txt +++ b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.reindex.txt @@ -42,12 +42,15 @@ class org.elasticsearch.script.ReindexScript { } class org.elasticsearch.script.field.WriteField { + String getName() boolean exists() WriteField move(def) WriteField overwrite(def) void remove() WriteField set(def) WriteField append(def) + boolean isEmpty() + int size() Iterator iterator() def get(def) def get(int, def) diff --git a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.update.txt b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.update.txt index 307fa18645eb5..5a94e6b6b402f 100644 --- a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.update.txt +++ b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.update.txt @@ -37,12 +37,15 @@ class org.elasticsearch.script.UpdateScript { } class org.elasticsearch.script.field.WriteField { + String getName() boolean exists() WriteField move(def) WriteField overwrite(def) void remove() WriteField set(def) WriteField append(def) + boolean isEmpty() + int size() Iterator iterator() def get(def) def get(int, def) diff --git a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.update_by_query.txt b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.update_by_query.txt index bd3dbfb675f11..e4fdbe1157294 100644 --- a/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.update_by_query.txt +++ b/modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.update_by_query.txt @@ -36,12 +36,15 @@ class org.elasticsearch.script.UpdateByQueryScript { } class org.elasticsearch.script.field.WriteField { + String getName() boolean exists() WriteField move(def) WriteField overwrite(def) void remove() WriteField set(def) WriteField append(def) + boolean isEmpty() + int size() Iterator iterator() def get(def) def get(int, def) diff --git a/server/src/main/java/org/elasticsearch/ingest/ConditionalProcessor.java b/server/src/main/java/org/elasticsearch/ingest/ConditionalProcessor.java index cdeda3e7a2efd..cf234dee1861b 100644 --- a/server/src/main/java/org/elasticsearch/ingest/ConditionalProcessor.java +++ b/server/src/main/java/org/elasticsearch/ingest/ConditionalProcessor.java @@ -11,7 +11,6 @@ import org.elasticsearch.common.logging.DeprecationCategory; import org.elasticsearch.common.logging.DeprecationLogger; -import org.elasticsearch.script.CtxMapWrapper; import org.elasticsearch.script.DynamicMap; import org.elasticsearch.script.IngestConditionalScript; import org.elasticsearch.script.Script; @@ -58,7 +57,6 @@ public class ConditionalProcessor extends AbstractProcessor implements WrappingP private final IngestMetric metric; private final LongSupplier relativeTimeProvider; private final IngestConditionalScript precompiledConditionScript; - private final CtxMapWrapper ctxMapWrapper; ConditionalProcessor(String tag, String description, Script script, ScriptService scriptService, Processor processor) { this(tag, description, script, scriptService, processor, System::nanoTime); @@ -78,12 +76,11 @@ public class ConditionalProcessor extends AbstractProcessor implements WrappingP this.processor = processor; this.metric = new IngestMetric(); this.relativeTimeProvider = relativeTimeProvider; - this.ctxMapWrapper = new CtxMapWrapper(); try { final IngestConditionalScript.Factory factory = scriptService.compile(script, IngestConditionalScript.CONTEXT); if (ScriptType.INLINE.equals(script.getType())) { - precompiledConditionScript = factory.newInstance(script.getParams(), ctxMapWrapper); + precompiledConditionScript = factory.newInstance(script.getParams()); } else { // stored script, so will have to compile at runtime precompiledConditionScript = null; @@ -147,14 +144,9 @@ boolean evaluate(IngestDocument ingestDocument) { IngestConditionalScript script = precompiledConditionScript; if (script == null) { IngestConditionalScript.Factory factory = scriptService.compile(condition, IngestConditionalScript.CONTEXT); - script = factory.newInstance(condition.getParams(), ctxMapWrapper); - } - ctxMapWrapper.setCtxMap(new UnmodifiableIngestData(new DynamicMap(ingestDocument.getSourceAndMetadata(), FUNCTIONS))); - try { - return script.execute(); - } finally { - ctxMapWrapper.clearCtxMap(); + script = factory.newInstance(condition.getParams()); } + return script.execute(new UnmodifiableIngestData(new DynamicMap(ingestDocument.getSourceAndMetadata(), FUNCTIONS))); } public Processor getInnerProcessor() { diff --git a/server/src/main/java/org/elasticsearch/script/CtxMapWrapper.java b/server/src/main/java/org/elasticsearch/script/CtxMapWrapper.java deleted file mode 100644 index 11241064573fe..0000000000000 --- a/server/src/main/java/org/elasticsearch/script/CtxMapWrapper.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -package org.elasticsearch.script; - -import java.util.Map; - -/** - * A wrapper for a {@link CtxMap} that allows for ad-hoc setting for script execution. - * This allows for precompilation of scripts that can be executed with different contexts. - * The wrapped {@link CtxMap} should be cleared after use to avoid leaks. - */ -public class CtxMapWrapper { - private Map ctxMap; - - public Map getCtxMap() { - if (ctxMap == null) { - throw new IllegalStateException("CtxMap is not set"); - } - return ctxMap; - } - - public void setCtxMap(Map ctxMap) { - this.ctxMap = ctxMap; - } - - public void clearCtxMap() { - this.ctxMap = null; - } -} diff --git a/server/src/main/java/org/elasticsearch/script/IngestConditionalScript.java b/server/src/main/java/org/elasticsearch/script/IngestConditionalScript.java index f15e72402dea2..1132264efe7a5 100644 --- a/server/src/main/java/org/elasticsearch/script/IngestConditionalScript.java +++ b/server/src/main/java/org/elasticsearch/script/IngestConditionalScript.java @@ -15,12 +15,10 @@ /** * A script used by {@link org.elasticsearch.ingest.ConditionalProcessor}. - * To properly expose the {@link SourceMapFieldScript#field(String)} API, make sure to provide a valid {@link CtxMap} before execution - * through the {@link CtxMapWrapper} passed to the constructor and make sure to clear it after use to avoid leaks. */ -public abstract class IngestConditionalScript extends SourceMapFieldScript { +public abstract class IngestConditionalScript { - public static final String[] PARAMETERS = {}; + public static final String[] PARAMETERS = { "ctx" }; /** The context used to compile {@link IngestConditionalScript} factories. */ public static final ScriptContext CONTEXT = new ScriptContext<>( @@ -35,8 +33,7 @@ public abstract class IngestConditionalScript extends SourceMapFieldScript { /** The generic runtime parameters for the script. */ private final Map params; - public IngestConditionalScript(Map params, CtxMapWrapper ctxMapWrapper) { - super(ctxMapWrapper); + public IngestConditionalScript(Map params) { this.params = params; } @@ -45,9 +42,9 @@ public Map getParams() { return params; } - public abstract boolean execute(); + public abstract boolean execute(Map ctx); public interface Factory { - IngestConditionalScript newInstance(Map params, CtxMapWrapper ctxMapWrapper); + IngestConditionalScript newInstance(Map params); } } diff --git a/server/src/main/java/org/elasticsearch/script/SourceMapFieldScript.java b/server/src/main/java/org/elasticsearch/script/SourceMapFieldScript.java deleted file mode 100644 index 2088cb577f4ea..0000000000000 --- a/server/src/main/java/org/elasticsearch/script/SourceMapFieldScript.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -package org.elasticsearch.script; - -import org.elasticsearch.script.field.SourceMapField; - -import java.util.Map; - -/** - * Abstract base class for scripts that read field values. - * These scripts provide {@code ctx} for backwards compatibility and expose {@link Metadata}. - */ -public abstract class SourceMapFieldScript { - protected final CtxMapWrapper ctxMapWrapper; - - public SourceMapFieldScript(CtxMapWrapper ctxMapWrapper) { - this.ctxMapWrapper = ctxMapWrapper; - } - - /** Provides backwards compatibility access to ctx */ - public Map getCtx() { - return ctxMapWrapper.getCtxMap(); - } - - public SourceMapField field(String path) { - return new SourceMapField(path, ctxMapWrapper::getCtxMap); - } -} diff --git a/server/src/main/java/org/elasticsearch/script/field/SourceMapField.java b/server/src/main/java/org/elasticsearch/script/field/SourceMapField.java deleted file mode 100644 index c2258fa903642..0000000000000 --- a/server/src/main/java/org/elasticsearch/script/field/SourceMapField.java +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -package org.elasticsearch.script.field; - -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.function.Predicate; -import java.util.function.Supplier; - -public class SourceMapField implements Field { - protected String path; - protected Supplier> rootSupplier; - - protected Map container; - protected String leaf; - - protected static final Object MISSING = new Object(); - - public SourceMapField(String path, Supplier> rootSupplier) { - this.path = path; - this.rootSupplier = rootSupplier; - resolveDepthFlat(); - } - - // Path Read - - /** - * Get the path represented by this Field - */ - public String getName() { - return path; - } - - /** - * Does the path exist? - */ - public boolean exists() { - return leaf != null && container.containsKey(leaf); - } - - /** - * Is this path associated with any values? - */ - @Override - public boolean isEmpty() { - return size() == 0; - } - - /** - * How many elements are at the leaf of this path? - */ - @Override - public int size() { - if (leaf == null) { - return 0; - } - - Object value = container.getOrDefault(leaf, MISSING); - if (value == MISSING) { - return 0; - } - - if (value instanceof List list) { - return list.size(); - } - return 1; - } - - /** - * Iterate through all elements of this path with an iterator that cannot mutate the underlying map. - */ - @Override - public Iterator iterator() { - if (leaf == null) { - return Collections.emptyIterator(); - } - - Object value = container.getOrDefault(leaf, MISSING); - if (value == MISSING) { - return Collections.emptyIterator(); - } - - if (value instanceof List list) { - return getListIterator(list); - } - return Collections.singleton(value).iterator(); - } - - /** - * Get an iterator for the given list that cannot mutate the underlying list. Subclasses can override this method to allow for - * mutating iterators. - * @param list the list to get an iterator for - * @return an iterator that cannot mutate the underlying list - */ - @SuppressWarnings("unchecked") - protected Iterator getListIterator(List list) { - return (Iterator) Collections.unmodifiableList(list).iterator(); - } - - /** - * Get the value at this path, if there is no value then get the provided {@param defaultValue} - */ - public Object get(Object defaultValue) { - if (leaf == null) { - return defaultValue; - } - - return container.getOrDefault(leaf, defaultValue); - } - - /** - * Get the value at the given index at this path or {@param defaultValue} if there is no such value. - */ - public Object get(int index, Object defaultValue) { - if (leaf == null) { - return defaultValue; - } - - Object value = container.getOrDefault(leaf, MISSING); - if (value instanceof List list) { - if (index < list.size()) { - return list.get(index); - } - } else if (value != MISSING && index == 0) { - return value; - } - - return defaultValue; - } - - /** - * Is there any value matching {@param predicate} at this path? - */ - public boolean hasValue(Predicate predicate) { - if (leaf == null) { - return false; - } - - Object value = container.getOrDefault(leaf, MISSING); - if (value == MISSING) { - return false; - } - - if (value instanceof List list) { - return list.stream().anyMatch(predicate); - } - - return predicate.test(value); - } - - /** - * Change the path and clear the existing resolution by setting {@link #leaf} and {@link #container} to null. - * Caller needs to re-resolve after this call. - */ - protected void setPath(String path) { - this.path = path; - this.leaf = null; - this.container = null; - } - - /** - * Resolve {@link #path} from the root. - * - * Tries to resolve the path one segment at a time, if the segment is not mapped to a Java Map, then - * treats that segment and the rest as the leaf if it resolves. - * - * a.b.c could be resolved as - * I) ['a']['b']['c'] if 'a' is a Map at the root and 'b' is a Map in 'a', 'c' need not exist in 'b'. - * II) ['a']['b.c'] if 'a' is a Map at the root and 'b' does not exist in 'a's Map but 'b.c' does. - * III) ['a.b.c'] if 'a' doesn't exist at the root but 'a.b.c' does. - * - * {@link #container} and {@link #leaf} and non-null if resolved. - */ - @SuppressWarnings("unchecked") - protected final void resolveDepthFlat() { - container = rootSupplier.get(); - - int index = path.indexOf('.'); - int lastIndex = 0; - String segment; - - while (index != -1) { - segment = path.substring(lastIndex, index); - Object value = container.get(segment); - if (value instanceof Map map) { - container = (Map) map; - lastIndex = index + 1; - index = path.indexOf('.', lastIndex); - } else { - // Check rest of segments as a single key - String rest = path.substring(lastIndex); - if (container.containsKey(rest)) { - leaf = rest; - } else { - leaf = null; - } - return; - } - } - leaf = path.substring(lastIndex); - } -} diff --git a/server/src/main/java/org/elasticsearch/script/field/WriteField.java b/server/src/main/java/org/elasticsearch/script/field/WriteField.java index 373a512ac865c..2bb062c8d8483 100644 --- a/server/src/main/java/org/elasticsearch/script/field/WriteField.java +++ b/server/src/main/java/org/elasticsearch/script/field/WriteField.java @@ -24,10 +24,35 @@ import java.util.function.Predicate; import java.util.function.Supplier; -public final class WriteField extends SourceMapField { +public final class WriteField implements Field { + private String path; + private Supplier> rootSupplier; + + private Map container; + private String leaf; + + private static final Object MISSING = new Object(); public WriteField(String path, Supplier> rootSupplier) { - super(path, rootSupplier); + this.path = path; + this.rootSupplier = rootSupplier; + resolveDepthFlat(); + } + + // Path Read + + /** + * Get the path represented by this Field + */ + public String getName() { + return path; + } + + /** + * Does the path exist? + */ + public boolean exists() { + return leaf != null && container.containsKey(leaf); } // Path Update @@ -200,10 +225,106 @@ public WriteField append(Object value) { return this; } - @SuppressWarnings("unchecked") + // Value Read + + /** + * Is this path associated with any values? + */ + @Override + public boolean isEmpty() { + return size() == 0; + } + + /** + * How many elements are at the leaf of this path? + */ + @Override + public int size() { + if (leaf == null) { + return 0; + } + + Object value = container.getOrDefault(leaf, MISSING); + if (value == MISSING) { + return 0; + } + + if (value instanceof List list) { + return list.size(); + } + return 1; + } + + /** + * Iterate through all elements of this path + */ @Override - protected Iterator getListIterator(List list) { - return (Iterator) list.iterator(); + @SuppressWarnings("unchecked") + public Iterator iterator() { + if (leaf == null) { + return Collections.emptyIterator(); + } + + Object value = container.getOrDefault(leaf, MISSING); + if (value == MISSING) { + return Collections.emptyIterator(); + } + + if (value instanceof List list) { + return (Iterator) list.iterator(); + } + return Collections.singleton(value).iterator(); + } + + /** + * Get the value at this path, if there is no value then get the provided {@param defaultValue} + */ + public Object get(Object defaultValue) { + if (leaf == null) { + return defaultValue; + } + + return container.getOrDefault(leaf, defaultValue); + } + + /** + * Get the value at the given index at this path or {@param defaultValue} if there is no such value. + */ + public Object get(int index, Object defaultValue) { + if (leaf == null) { + return defaultValue; + } + + Object value = container.getOrDefault(leaf, MISSING); + if (value instanceof List list) { + if (index < list.size()) { + return list.get(index); + } + } else if (value != MISSING && index == 0) { + return value; + } + + return defaultValue; + } + + /** + * Is there any value matching {@param predicate} at this path? + */ + public boolean hasValue(Predicate predicate) { + if (leaf == null) { + return false; + } + + Object value = container.getOrDefault(leaf, MISSING); + if (value == MISSING) { + return false; + } + + if (value instanceof List list) { + return list.stream().anyMatch(predicate); + } + + return predicate.test(value); } // Value Update @@ -479,6 +600,16 @@ public void remove() { throw new IllegalStateException("Unexpected value [" + value + "] of type [" + typeName(value) + "] at [" + path + "] for docs()"); } + /** + * Change the path and clear the existing resolution by setting {@link #leaf} and {@link #container} to null. + * Caller needs to re-resolve after this call. + */ + private void setPath(String path) { + this.path = path; + this.leaf = null; + this.container = null; + } + /** * Get the path to a leaf or create it if one does not exist. */ @@ -491,6 +622,48 @@ private void setLeaf() { } } + /** + * Resolve {@link #path} from the root. + * + * Tries to resolve the path one segment at a time, if the segment is not mapped to a Java Map, then + * treats that segment and the rest as the leaf if it resolves. + * + * a.b.c could be resolved as + * I) ['a']['b']['c'] if 'a' is a Map at the root and 'b' is a Map in 'a', 'c' need not exist in 'b'. + * II) ['a']['b.c'] if 'a' is a Map at the root and 'b' does not exist in 'a's Map but 'b.c' does. + * III) ['a.b.c'] if 'a' doesn't exist at the root but 'a.b.c' does. + * + * {@link #container} and {@link #leaf} and non-null if resolved. + */ + @SuppressWarnings("unchecked") + private void resolveDepthFlat() { + container = rootSupplier.get(); + + int index = path.indexOf('.'); + int lastIndex = 0; + String segment; + + while (index != -1) { + segment = path.substring(lastIndex, index); + Object value = container.get(segment); + if (value instanceof Map map) { + container = (Map) map; + lastIndex = index + 1; + index = path.indexOf('.', lastIndex); + } else { + // Check rest of segments as a single key + String rest = path.substring(lastIndex); + if (container.containsKey(rest)) { + leaf = rest; + } else { + leaf = null; + } + return; + } + } + leaf = path.substring(lastIndex); + } + /** * Create a new Map for each segment in path, if that segment is unmapped or mapped to null. * diff --git a/server/src/test/java/org/elasticsearch/ingest/ConditionalProcessorTests.java b/server/src/test/java/org/elasticsearch/ingest/ConditionalProcessorTests.java index 6f1f57bcd67ee..fe1300b1d2645 100644 --- a/server/src/test/java/org/elasticsearch/ingest/ConditionalProcessorTests.java +++ b/server/src/test/java/org/elasticsearch/ingest/ConditionalProcessorTests.java @@ -206,9 +206,9 @@ public void testRuntimeCompileError() { if (fail.get()) { throw new ScriptException("bad script", new ParseException("error", 0), List.of(), "", "lang", null); } else { - return (params, ctxMap) -> new IngestConditionalScript(params, ctxMap) { + return params -> new IngestConditionalScript(params) { @Override - public boolean execute() { + public boolean execute(Map ctx) { return false; } }; @@ -226,9 +226,9 @@ public boolean execute() { public void testRuntimeError() { ScriptService scriptService = MockScriptService.singleContext( IngestConditionalScript.CONTEXT, - code -> (params, ctxMapWrapper) -> new IngestConditionalScript(params, ctxMapWrapper) { + code -> params -> new IngestConditionalScript(params) { @Override - public boolean execute() { + public boolean execute(Map ctx) { throw new IllegalArgumentException("runtime problem"); } }, diff --git a/server/src/test/java/org/elasticsearch/script/field/SourceMapFieldTests.java b/server/src/test/java/org/elasticsearch/script/field/SourceMapFieldTests.java deleted file mode 100644 index 3fad8ff94c5b1..0000000000000 --- a/server/src/test/java/org/elasticsearch/script/field/SourceMapFieldTests.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -package org.elasticsearch.script.field; - -import org.elasticsearch.test.ESTestCase; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -public class SourceMapFieldTests extends ESTestCase { - - public void testResolveDepthFlat() { - Map map = new HashMap<>(); - map.put("abc.d.ef", "flat"); - - Map abc = new HashMap<>(); - map.put("abc", abc); - abc.put("d.ef", "mixed"); - - Map d = new HashMap<>(); - abc.put("d", d); - d.put("ef", "nested"); - - // { "abc.d.ef", "flat", "abc": { "d.ef": "mixed", "d": { "ef": "nested" } } } - SourceMapField field = new SourceMapField("abc.d.ef", () -> map); - assertTrue(field.exists()); - - assertEquals("nested", field.get("missing")); - // { "abc.d.ef", "flat", "abc": { "d.ef": "mixed", "d": { } } } - d.remove("ef"); - assertEquals("missing", field.get("missing")); - // { "abc.d.ef", "flat", "abc": { "d.ef": "mixed" } - // TODO(stu): this should be inaccessible - abc.remove("d"); - assertEquals("missing", field.get("missing")); - - // resolution at construction time - field = new SourceMapField("abc.d.ef", () -> map); - assertEquals("mixed", field.get("missing")); - abc.remove("d.ef"); - assertEquals("missing", field.get("missing")); - - field = new SourceMapField("abc.d.ef", () -> map); - // abc is still there - assertEquals("missing", field.get("missing")); - map.remove("abc"); - assertEquals("missing", field.get("missing")); - - field = new SourceMapField("abc.d.ef", () -> map); - assertEquals("flat", field.get("missing")); - } - - public void testExists() { - Map a = new HashMap<>(); - a.put("b.c", null); - assertTrue(new SourceMapField("a.b.c", () -> Map.of("a", a)).exists()); - - a.clear(); - Map level1 = new HashMap<>(); - level1.put("null", null); - a.put("level1", level1); - a.put("null", null); - // SourceMapField.leaf is null - assertFalse(new SourceMapField("missing.leaf", () -> a).exists()); - - // SourceMapField.leaf non-null but missing - assertFalse(new SourceMapField("missing", () -> a).exists()); - - // Check mappings with null values exist - assertTrue(new SourceMapField("level1.null", () -> a).exists()); - assertTrue(new SourceMapField("null", () -> a).exists()); - } - - public void testSizeIsEmpty() { - Map root = new HashMap<>(); - SourceMapField field = new SourceMapField("a.b.c", () -> root); - assertTrue(field.isEmpty()); - assertEquals(0, field.size()); - - root.put("a.b.c", List.of(1, 2)); - field = new SourceMapField("a.b.c", () -> root); - assertFalse(field.isEmpty()); - assertEquals(2, field.size()); - - Map d = new HashMap<>(); - root.put("d", d); - field = new SourceMapField("d.e", () -> root); - assertTrue(field.isEmpty()); - assertEquals(0, field.size()); - d.put("e", "foo"); - assertFalse(field.isEmpty()); - assertEquals(1, field.size()); - } - - public void testIterator() { - Map root = new HashMap<>(); - Map a = new HashMap<>(); - Map b = new HashMap<>(); - a.put("b", b); - root.put("a", a); - - SourceMapField field = new SourceMapField("a.b.c", () -> root); - assertFalse(field.iterator().hasNext()); - - b.put("c", "value"); - Iterator it = field.iterator(); - assertTrue(it.hasNext()); - assertEquals("value", it.next()); - assertFalse(it.hasNext()); - - b.put("c", List.of(1, 2, 3)); - it = field.iterator(); - assertTrue(it.hasNext()); - assertEquals(1, it.next()); - assertTrue(it.hasNext()); - assertEquals(2, it.next()); - assertTrue(it.hasNext()); - assertEquals(3, it.next()); - assertFalse(it.hasNext()); - - assertFalse(new SourceMapField("dne.dne", () -> root).iterator().hasNext()); - } - - @SuppressWarnings("unchecked") - public void testHasValue() { - Map root = new HashMap<>(); - Map a = new HashMap<>(); - Map b = new HashMap<>(); - a.put("b", b); - root.put("a", a); - b.put("c", new ArrayList<>(List.of(10, 11, 12))); - SourceMapField field = new SourceMapField("a.b.c", () -> root); - assertFalse(field.hasValue(v -> (Integer) v < 10)); - assertTrue(field.hasValue(v -> (Integer) v <= 10)); - - root.clear(); - a.clear(); - a.put("null", null); - a.put("b", List.of(1, 2, 3, 4)); - root.put("a", a); - field = new SourceMapField("a.b", () -> root); - assertTrue(field.hasValue(x -> (Integer) x % 2 == 0)); - assertFalse(field.hasValue(x -> (Integer) x > 4)); - assertFalse(new SourceMapField("d.e", () -> root).hasValue(Objects::isNull)); - assertTrue(new SourceMapField("a.null", () -> root).hasValue(Objects::isNull)); - assertFalse(new SourceMapField("a.null2", () -> root).hasValue(Objects::isNull)); - } - - public void testGetIndex() { - Map root = new HashMap<>(); - root.put("a", Map.of("b", List.of(1, 2, 3, 5), "c", "foo")); - SourceMapField field = new SourceMapField("a.b", () -> root); - assertEquals(5, field.get(3, 100)); - assertEquals(100, new SourceMapField("c.d", () -> root).get(3, 100)); - assertEquals("bar", new SourceMapField("a.c", () -> root).get(1, "bar")); - assertEquals("foo", new SourceMapField("a.c", () -> root).get(0, "bar")); - } -} diff --git a/server/src/test/java/org/elasticsearch/script/field/WriteFieldTests.java b/server/src/test/java/org/elasticsearch/script/field/WriteFieldTests.java index 39576a7564124..d50de79f8d84d 100644 --- a/server/src/test/java/org/elasticsearch/script/field/WriteFieldTests.java +++ b/server/src/test/java/org/elasticsearch/script/field/WriteFieldTests.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; +import java.util.Objects; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; @@ -28,6 +29,68 @@ public class WriteFieldTests extends ESTestCase { + public void testResolveDepthFlat() { + Map map = new HashMap<>(); + map.put("abc.d.ef", "flat"); + + Map abc = new HashMap<>(); + map.put("abc", abc); + abc.put("d.ef", "mixed"); + + Map d = new HashMap<>(); + abc.put("d", d); + d.put("ef", "nested"); + + // { "abc.d.ef", "flat", "abc": { "d.ef": "mixed", "d": { "ef": "nested" } } } + WriteField wf = new WriteField("abc.d.ef", () -> map); + assertTrue(wf.exists()); + + assertEquals("nested", wf.get("missing")); + // { "abc.d.ef", "flat", "abc": { "d.ef": "mixed", "d": { } } } + d.remove("ef"); + assertEquals("missing", wf.get("missing")); + // { "abc.d.ef", "flat", "abc": { "d.ef": "mixed" } + // TODO(stu): this should be inaccessible + abc.remove("d"); + assertEquals("missing", wf.get("missing")); + + // resolution at construction time + wf = new WriteField("abc.d.ef", () -> map); + assertEquals("mixed", wf.get("missing")); + abc.remove("d.ef"); + assertEquals("missing", wf.get("missing")); + + wf = new WriteField("abc.d.ef", () -> map); + // abc is still there + assertEquals("missing", wf.get("missing")); + map.remove("abc"); + assertEquals("missing", wf.get("missing")); + + wf = new WriteField("abc.d.ef", () -> map); + assertEquals("flat", wf.get("missing")); + } + + public void testExists() { + Map a = new HashMap<>(); + a.put("b.c", null); + assertTrue(new WriteField("a.b.c", () -> Map.of("a", a)).exists()); + + a.clear(); + Map level1 = new HashMap<>(); + level1.put("null", null); + a.put("level1", level1); + a.put("null", null); + // WriteField.leaf is null + assertFalse(new WriteField("missing.leaf", () -> a).exists()); + + // WriteField.leaf non-null but missing + assertFalse(new WriteField("missing", () -> a).exists()); + + // Check mappings with null values exist + assertTrue(new WriteField("level1.null", () -> a).exists()); + assertTrue(new WriteField("null", () -> a).exists()); + } + public void testMoveString() { String src = "a.b.c"; String dst = "d.e.f"; @@ -334,6 +397,56 @@ public void testAppend() { assertEquals(new ArrayList<>(List.of("bar")), b.get("c")); } + public void testSizeIsEmpty() { + Map root = new HashMap<>(); + WriteField wf = new WriteField("a.b.c", () -> root); + assertTrue(wf.isEmpty()); + assertEquals(0, wf.size()); + + root.put("a.b.c", List.of(1, 2)); + wf = new WriteField("a.b.c", () -> root); + assertFalse(wf.isEmpty()); + assertEquals(2, wf.size()); + + Map d = new HashMap<>(); + root.put("d", d); + wf = new WriteField("d.e", () -> root); + assertTrue(wf.isEmpty()); + assertEquals(0, wf.size()); + d.put("e", "foo"); + assertFalse(wf.isEmpty()); + assertEquals(1, wf.size()); + } + + public void testIterator() { + Map root = new HashMap<>(); + Map a = new HashMap<>(); + Map b = new HashMap<>(); + a.put("b", b); + root.put("a", a); + + WriteField wf = new WriteField("a.b.c", () -> root); + assertFalse(wf.iterator().hasNext()); + + b.put("c", "value"); + Iterator it = wf.iterator(); + assertTrue(it.hasNext()); + assertEquals("value", it.next()); + assertFalse(it.hasNext()); + + b.put("c", List.of(1, 2, 3)); + it = wf.iterator(); + assertTrue(it.hasNext()); + assertEquals(1, it.next()); + assertTrue(it.hasNext()); + assertEquals(2, it.next()); + assertTrue(it.hasNext()); + assertEquals(3, it.next()); + assertFalse(it.hasNext()); + + assertFalse(new WriteField("dne.dne", () -> root).iterator().hasNext()); + } + @SuppressWarnings("unchecked") public void testDeduplicate() { Map root = new HashMap<>(); @@ -416,6 +529,42 @@ public void testRemoveValuesIf() { assertNull(wf.get(null)); } + public void testHasValue() { + Map root = new HashMap<>(); + Map a = new HashMap<>(); + Map b = new HashMap<>(); + a.put("b", b); + root.put("a", a); + b.put("c", new ArrayList<>(List.of(10, 11, 12))); + WriteField wf = new WriteField("a.b.c", () -> root); + assertFalse(wf.hasValue(v -> (Integer) v < 10)); + assertTrue(wf.hasValue(v -> (Integer) v <= 10)); + wf.append(9); + assertTrue(wf.hasValue(v -> (Integer) v < 10)); + + root.clear(); + a.clear(); + a.put("null", null); + a.put("b", List.of(1, 2, 3, 4)); + root.put("a", a); + wf = new WriteField("a.b", () -> root); + assertTrue(wf.hasValue(x -> (Integer) x % 2 == 0)); + assertFalse(wf.hasValue(x -> (Integer) x > 4)); + assertFalse(new WriteField("d.e", () -> root).hasValue(Objects::isNull)); + assertTrue(new WriteField("a.null", () -> root).hasValue(Objects::isNull)); + assertFalse(new WriteField("a.null2", () -> root).hasValue(Objects::isNull)); + } + + public void testGetIndex() { + Map root = new HashMap<>(); + root.put("a", Map.of("b", List.of(1, 2, 3, 5), "c", "foo")); + WriteField wf = new WriteField("a.b", () -> root); + assertEquals(5, wf.get(3, 100)); + assertEquals(100, new WriteField("c.d", () -> root).get(3, 100)); + assertEquals("bar", new WriteField("a.c", () -> root).get(1, "bar")); + assertEquals("foo", new WriteField("a.c", () -> root).get(0, "bar")); + } + @SuppressWarnings("unchecked") public void testDoc() { Map root = new HashMap<>(); diff --git a/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java b/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java index 94828ef147898..24d46b99b541b 100644 --- a/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java +++ b/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java @@ -169,13 +169,10 @@ public void execute() { } else if (context.instanceClazz.equals(AggregationScript.class)) { return context.factoryClazz.cast(new MockAggregationScript(script)); } else if (context.instanceClazz.equals(IngestConditionalScript.class)) { - IngestConditionalScript.Factory factory = (parameters, ctxMapWrapper) -> new IngestConditionalScript( - parameters, - ctxMapWrapper - ) { + IngestConditionalScript.Factory factory = parameters -> new IngestConditionalScript(parameters) { @Override - public boolean execute() { - return (boolean) script.apply(ctxMapWrapper.getCtxMap()); + public boolean execute(Map ctx) { + return (boolean) script.apply(ctx); } }; return context.factoryClazz.cast(factory);