Skip to content

Commit bfc1635

Browse files
committed
Separate field access and field modification APIs
1 parent d7fbae9 commit bfc1635

File tree

5 files changed

+247
-184
lines changed

5 files changed

+247
-184
lines changed

modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.processor_conditional.txt

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,37 +10,16 @@
1010
# This file contains a whitelist for conditional ingest scripts
1111

1212
class org.elasticsearch.script.IngestConditionalScript {
13-
WriteField field(String)
13+
SourceMapField field(String)
1414
}
1515

16-
class org.elasticsearch.script.field.WriteField {
16+
class org.elasticsearch.script.field.SourceMapField {
1717
String getName()
1818
boolean exists()
19-
WriteField move(def)
20-
WriteField overwrite(def)
21-
void remove()
22-
WriteField set(def)
23-
WriteField append(def)
2419
boolean isEmpty()
2520
int size()
2621
Iterator iterator()
2722
def get(def)
2823
def get(int, def)
2924
boolean hasValue(Predicate)
30-
WriteField transform(Function)
31-
WriteField deduplicate()
32-
WriteField removeValuesIf(Predicate)
33-
WriteField removeValue(int)
34-
NestedDocument doc()
35-
NestedDocument doc(int)
36-
Iterable docs()
37-
}
38-
39-
class org.elasticsearch.script.field.NestedDocument {
40-
WriteField field(String)
41-
Stream fields(String)
42-
boolean isEmpty()
43-
int size()
44-
boolean exists()
45-
void remove()
4625
}

server/src/main/java/org/elasticsearch/script/IngestConditionalScript.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
/**
1717
* A script used by {@link org.elasticsearch.ingest.ConditionalProcessor}.
1818
*/
19-
public abstract class IngestConditionalScript extends WriteScript {
19+
public abstract class IngestConditionalScript extends SourceMapFieldScript {
2020

2121
public static final String[] PARAMETERS = { "ctxdsf" /*todo change param name*/ };
2222

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.script;
11+
12+
import org.elasticsearch.script.field.SourceMapField;
13+
14+
import java.util.Map;
15+
16+
/**
17+
* Abstract base class for scripts that read field values.
18+
* These scripts provide {@code ctx} for backwards compatibility and expose {@link Metadata}.
19+
*/
20+
public abstract class SourceMapFieldScript {
21+
protected final CtxMap<?> ctxMap;
22+
23+
public SourceMapFieldScript(CtxMap<?> ctxMap) {
24+
this.ctxMap = ctxMap;
25+
}
26+
27+
/** Provides backwards compatibility access to ctx */
28+
public Map<String, Object> getCtx() {
29+
return ctxMap;
30+
}
31+
32+
/** Return the metadata for this script */
33+
public Metadata metadata() {
34+
return ctxMap.getMetadata();
35+
}
36+
37+
public SourceMapField field(String path) {
38+
return new SourceMapField(path, ctxMap::getSource);
39+
}
40+
}
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.script.field;
11+
12+
import java.util.Collections;
13+
import java.util.Iterator;
14+
import java.util.List;
15+
import java.util.Map;
16+
import java.util.function.Predicate;
17+
import java.util.function.Supplier;
18+
19+
public class SourceMapField implements Field<Object> {
20+
protected String path;
21+
protected Supplier<Map<String, Object>> rootSupplier;
22+
23+
protected Map<String, Object> container;
24+
protected String leaf;
25+
26+
protected static final Object MISSING = new Object();
27+
28+
public SourceMapField(String path, Supplier<Map<String, Object>> rootSupplier) {
29+
this.path = path;
30+
this.rootSupplier = rootSupplier;
31+
resolveDepthFlat();
32+
}
33+
34+
// Path Read
35+
36+
/**
37+
* Get the path represented by this Field
38+
*/
39+
public String getName() {
40+
return path;
41+
}
42+
43+
/**
44+
* Does the path exist?
45+
*/
46+
public boolean exists() {
47+
return leaf != null && container.containsKey(leaf);
48+
}
49+
50+
/**
51+
* Is this path associated with any values?
52+
*/
53+
@Override
54+
public boolean isEmpty() {
55+
return size() == 0;
56+
}
57+
58+
/**
59+
* How many elements are at the leaf of this path?
60+
*/
61+
@Override
62+
public int size() {
63+
if (leaf == null) {
64+
return 0;
65+
}
66+
67+
Object value = container.getOrDefault(leaf, MISSING);
68+
if (value == MISSING) {
69+
return 0;
70+
}
71+
72+
if (value instanceof List<?> list) {
73+
return list.size();
74+
}
75+
return 1;
76+
}
77+
78+
/**
79+
* Iterate through all elements of this path with an iterator that cannot mutate the underlying map.
80+
*/
81+
@Override
82+
@SuppressWarnings("unchecked")
83+
public Iterator<Object> iterator() {
84+
if (leaf == null) {
85+
return Collections.emptyIterator();
86+
}
87+
88+
Object value = container.getOrDefault(leaf, MISSING);
89+
if (value == MISSING) {
90+
return Collections.emptyIterator();
91+
}
92+
93+
if (value instanceof List<?> list) {
94+
// todo - revisit whether this is how we want to guarantee immutability
95+
return (Iterator<Object>) Collections.unmodifiableList(list).iterator();
96+
}
97+
return Collections.singleton(value).iterator();
98+
}
99+
100+
/**
101+
* Get the value at this path, if there is no value then get the provided {@param defaultValue}
102+
*/
103+
public Object get(Object defaultValue) {
104+
if (leaf == null) {
105+
return defaultValue;
106+
}
107+
108+
return container.getOrDefault(leaf, defaultValue);
109+
}
110+
111+
/**
112+
* Get the value at the given index at this path or {@param defaultValue} if there is no such value.
113+
*/
114+
public Object get(int index, Object defaultValue) {
115+
if (leaf == null) {
116+
return defaultValue;
117+
}
118+
119+
Object value = container.getOrDefault(leaf, MISSING);
120+
if (value instanceof List<?> list) {
121+
if (index < list.size()) {
122+
return list.get(index);
123+
}
124+
} else if (value != MISSING && index == 0) {
125+
return value;
126+
}
127+
128+
return defaultValue;
129+
}
130+
131+
/**
132+
* Is there any value matching {@param predicate} at this path?
133+
*/
134+
public boolean hasValue(Predicate<Object> predicate) {
135+
if (leaf == null) {
136+
return false;
137+
}
138+
139+
Object value = container.getOrDefault(leaf, MISSING);
140+
if (value == MISSING) {
141+
return false;
142+
}
143+
144+
if (value instanceof List<?> list) {
145+
return list.stream().anyMatch(predicate);
146+
}
147+
148+
return predicate.test(value);
149+
}
150+
151+
/**
152+
* Change the path and clear the existing resolution by setting {@link #leaf} and {@link #container} to null.
153+
* Caller needs to re-resolve after this call.
154+
*/
155+
protected void setPath(String path) {
156+
this.path = path;
157+
this.leaf = null;
158+
this.container = null;
159+
}
160+
161+
/**
162+
* Resolve {@link #path} from the root.
163+
*
164+
* Tries to resolve the path one segment at a time, if the segment is not mapped to a Java Map, then
165+
* treats that segment and the rest as the leaf if it resolves.
166+
*
167+
* a.b.c could be resolved as
168+
* 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'.
169+
* 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.
170+
* III) ['a.b.c'] if 'a' doesn't exist at the root but 'a.b.c' does.
171+
*
172+
* {@link #container} and {@link #leaf} and non-null if resolved.
173+
*/
174+
@SuppressWarnings("unchecked")
175+
protected final void resolveDepthFlat() {
176+
container = rootSupplier.get();
177+
178+
int index = path.indexOf('.');
179+
int lastIndex = 0;
180+
String segment;
181+
182+
while (index != -1) {
183+
segment = path.substring(lastIndex, index);
184+
Object value = container.get(segment);
185+
if (value instanceof Map<?, ?> map) {
186+
container = (Map<String, Object>) map;
187+
lastIndex = index + 1;
188+
index = path.indexOf('.', lastIndex);
189+
} else {
190+
// Check rest of segments as a single key
191+
String rest = path.substring(lastIndex);
192+
if (container.containsKey(rest)) {
193+
leaf = rest;
194+
} else {
195+
leaf = null;
196+
}
197+
return;
198+
}
199+
}
200+
leaf = path.substring(lastIndex);
201+
}
202+
}

0 commit comments

Comments
 (0)