Skip to content

Commit b52818e

Browse files
authored
Painless: Add Bindings (#33042)
Add bindings that allow some specialized methods to store permanent state between script executions.
1 parent 6daf811 commit b52818e

File tree

19 files changed

+712
-91
lines changed

19 files changed

+712
-91
lines changed

modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/Whitelist.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,12 @@ public final class Whitelist {
6161
/** The {@link List} of all the whitelisted Painless classes. */
6262
public final List<WhitelistClass> whitelistClasses;
6363

64+
public final List<WhitelistBinding> whitelistBindings;
65+
6466
/** Standard constructor. All values must be not {@code null}. */
65-
public Whitelist(ClassLoader classLoader, List<WhitelistClass> whitelistClasses) {
67+
public Whitelist(ClassLoader classLoader, List<WhitelistClass> whitelistClasses, List<WhitelistBinding> whitelistBindings) {
6668
this.classLoader = Objects.requireNonNull(classLoader);
6769
this.whitelistClasses = Collections.unmodifiableList(Objects.requireNonNull(whitelistClasses));
70+
this.whitelistBindings = Collections.unmodifiableList(Objects.requireNonNull(whitelistBindings));
6871
}
6972
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.painless.spi;
21+
22+
import java.util.List;
23+
import java.util.Objects;
24+
25+
/**
26+
* A binding represents a method call that stores state. Each binding class must have exactly one
27+
* public constructor and one public method excluding those inherited directly from {@link Object}.
28+
* The canonical type name parameters provided must match those of the constructor and method combined.
29+
* The constructor for a binding class will be called when the binding method is called for the first
30+
* time at which point state may be stored for the arguments passed into the constructor. The method
31+
* for a binding class will be called each time the binding method is called and may use the previously
32+
* stored state.
33+
*/
34+
public class WhitelistBinding {
35+
36+
/** Information about where this constructor was whitelisted from. */
37+
public final String origin;
38+
39+
/** The Java class name this binding represents. */
40+
public final String targetJavaClassName;
41+
42+
/** The method name for this binding. */
43+
public final String methodName;
44+
45+
/**
46+
* The canonical type name for the return type.
47+
*/
48+
public final String returnCanonicalTypeName;
49+
50+
/**
51+
* A {@link List} of {@link String}s that are the Painless type names for the parameters of the
52+
* constructor which can be used to look up the Java constructor through reflection.
53+
*/
54+
public final List<String> canonicalTypeNameParameters;
55+
56+
/** Standard constructor. All values must be not {@code null}. */
57+
public WhitelistBinding(String origin, String targetJavaClassName,
58+
String methodName, String returnCanonicalTypeName, List<String> canonicalTypeNameParameters) {
59+
60+
this.origin = Objects.requireNonNull(origin);
61+
this.targetJavaClassName = Objects.requireNonNull(targetJavaClassName);
62+
63+
this.methodName = Objects.requireNonNull(methodName);
64+
this.returnCanonicalTypeName = Objects.requireNonNull(returnCanonicalTypeName);
65+
this.canonicalTypeNameParameters = Objects.requireNonNull(canonicalTypeNameParameters);
66+
}
67+
}

modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistClass.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,8 @@ public final class WhitelistClass {
6262

6363
/** Standard constructor. All values must be not {@code null}. */
6464
public WhitelistClass(String origin, String javaClassName, boolean noImport,
65-
List<WhitelistConstructor> whitelistConstructors,
66-
List<WhitelistMethod> whitelistMethods,
67-
List<WhitelistField> whitelistFields) {
65+
List<WhitelistConstructor> whitelistConstructors, List<WhitelistMethod> whitelistMethods, List<WhitelistField> whitelistFields)
66+
{
6867

6968
this.origin = Objects.requireNonNull(origin);
7069
this.javaClassName = Objects.requireNonNull(javaClassName);

modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistLoader.java

Lines changed: 127 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ public final class WhitelistLoader {
133133
*/
134134
public static Whitelist loadFromResourceFiles(Class<?> resource, String... filepaths) {
135135
List<WhitelistClass> whitelistClasses = new ArrayList<>();
136+
List<WhitelistBinding> whitelistBindings = new ArrayList<>();
136137

137138
// Execute a single pass through the whitelist text files. This will gather all the
138139
// constructors, methods, augmented methods, and fields for each whitelisted class.
@@ -141,8 +142,9 @@ public static Whitelist loadFromResourceFiles(Class<?> resource, String... filep
141142
int number = -1;
142143

143144
try (LineNumberReader reader = new LineNumberReader(
144-
new InputStreamReader(resource.getResourceAsStream(filepath), StandardCharsets.UTF_8))) {
145+
new InputStreamReader(resource.getResourceAsStream(filepath), StandardCharsets.UTF_8))) {
145146

147+
String parseType = null;
146148
String whitelistClassOrigin = null;
147149
String javaClassName = null;
148150
boolean noImport = false;
@@ -165,7 +167,11 @@ public static Whitelist loadFromResourceFiles(Class<?> resource, String... filep
165167
// Ensure the final token of the line is '{'.
166168
if (line.endsWith("{") == false) {
167169
throw new IllegalArgumentException(
168-
"invalid class definition: failed to parse class opening bracket [" + line + "]");
170+
"invalid class definition: failed to parse class opening bracket [" + line + "]");
171+
}
172+
173+
if (parseType != null) {
174+
throw new IllegalArgumentException("invalid definition: cannot embed class definition [" + line + "]");
169175
}
170176

171177
// Parse the Java class name.
@@ -178,41 +184,125 @@ public static Whitelist loadFromResourceFiles(Class<?> resource, String... filep
178184
throw new IllegalArgumentException("invalid class definition: failed to parse class name [" + line + "]");
179185
}
180186

187+
parseType = "class";
181188
whitelistClassOrigin = "[" + filepath + "]:[" + number + "]";
182189
javaClassName = tokens[0];
183190

184191
// Reset all the constructors, methods, and fields to support a new class.
185192
whitelistConstructors = new ArrayList<>();
186193
whitelistMethods = new ArrayList<>();
187194
whitelistFields = new ArrayList<>();
195+
} else if (line.startsWith("static ")) {
196+
// Ensure the final token of the line is '{'.
197+
if (line.endsWith("{") == false) {
198+
throw new IllegalArgumentException(
199+
"invalid static definition: failed to parse static opening bracket [" + line + "]");
200+
}
188201

189-
// Handle the end of a class, by creating a new WhitelistClass with all the previously gathered
190-
// constructors, methods, augmented methods, and fields, and adding it to the list of whitelisted classes.
202+
if (parseType != null) {
203+
throw new IllegalArgumentException("invalid definition: cannot embed static definition [" + line + "]");
204+
}
205+
206+
parseType = "static";
207+
208+
// Handle the end of a definition and reset all previously gathered values.
191209
// Expects the following format: '}' '\n'
192210
} else if (line.equals("}")) {
193-
if (javaClassName == null) {
194-
throw new IllegalArgumentException("invalid class definition: extraneous closing bracket");
211+
if (parseType == null) {
212+
throw new IllegalArgumentException("invalid definition: extraneous closing bracket");
195213
}
196214

197-
whitelistClasses.add(new WhitelistClass(whitelistClassOrigin, javaClassName, noImport,
198-
whitelistConstructors, whitelistMethods, whitelistFields));
215+
// Create a new WhitelistClass with all the previously gathered constructors, methods,
216+
// augmented methods, and fields, and add it to the list of whitelisted classes.
217+
if ("class".equals(parseType)) {
218+
whitelistClasses.add(new WhitelistClass(whitelistClassOrigin, javaClassName, noImport,
219+
whitelistConstructors, whitelistMethods, whitelistFields));
220+
221+
whitelistClassOrigin = null;
222+
javaClassName = null;
223+
noImport = false;
224+
whitelistConstructors = null;
225+
whitelistMethods = null;
226+
whitelistFields = null;
227+
}
199228

200-
// Set all the variables to null to ensure a new class definition is found before other parsable values.
201-
whitelistClassOrigin = null;
202-
javaClassName = null;
203-
noImport = false;
204-
whitelistConstructors = null;
205-
whitelistMethods = null;
206-
whitelistFields = null;
229+
// Reset the parseType.
230+
parseType = null;
207231

208-
// Handle all other valid cases.
209-
} else {
232+
// Handle static definition types.
233+
// Expects the following format: ID ID '(' ( ID ( ',' ID )* )? ')' 'bound_to' ID '\n'
234+
} else if ("static".equals(parseType)) {
235+
// Mark the origin of this parsable object.
236+
String origin = "[" + filepath + "]:[" + number + "]";
237+
238+
// Parse the tokens prior to the method parameters.
239+
int parameterStartIndex = line.indexOf('(');
240+
241+
if (parameterStartIndex == -1) {
242+
throw new IllegalArgumentException(
243+
"illegal static definition: start of method parameters not found [" + line + "]");
244+
}
245+
246+
String[] tokens = line.substring(0, parameterStartIndex).trim().split("\\s+");
247+
248+
String methodName;
249+
250+
// Based on the number of tokens, look up the Java method name.
251+
if (tokens.length == 2) {
252+
methodName = tokens[1];
253+
} else {
254+
throw new IllegalArgumentException("invalid method definition: unexpected format [" + line + "]");
255+
}
256+
257+
String returnCanonicalTypeName = tokens[0];
258+
259+
// Parse the method parameters.
260+
int parameterEndIndex = line.indexOf(')');
261+
262+
if (parameterEndIndex == -1) {
263+
throw new IllegalArgumentException(
264+
"illegal static definition: end of method parameters not found [" + line + "]");
265+
}
266+
267+
String[] canonicalTypeNameParameters =
268+
line.substring(parameterStartIndex + 1, parameterEndIndex).replaceAll("\\s+", "").split(",");
269+
270+
// Handle the case for a method with no parameters.
271+
if ("".equals(canonicalTypeNameParameters[0])) {
272+
canonicalTypeNameParameters = new String[0];
273+
}
274+
275+
// Parse the static type and class.
276+
tokens = line.substring(parameterEndIndex + 1).trim().split("\\s+");
277+
278+
String staticType;
279+
String targetJavaClassName;
280+
281+
// Based on the number of tokens, look up the type and class.
282+
if (tokens.length == 2) {
283+
staticType = tokens[0];
284+
targetJavaClassName = tokens[1];
285+
} else {
286+
throw new IllegalArgumentException("invalid static definition: unexpected format [" + line + "]");
287+
}
288+
289+
// Check the static type is valid.
290+
if ("bound_to".equals(staticType) == false) {
291+
throw new IllegalArgumentException(
292+
"invalid static definition: unexpected static type [" + staticType + "] [" + line + "]");
293+
}
294+
295+
whitelistBindings.add(new WhitelistBinding(origin, targetJavaClassName,
296+
methodName, returnCanonicalTypeName, Arrays.asList(canonicalTypeNameParameters)));
297+
298+
// Handle class definition types.
299+
} else if ("class".equals(parseType)) {
210300
// Mark the origin of this parsable object.
211301
String origin = "[" + filepath + "]:[" + number + "]";
212302

213303
// Ensure we have a defined class before adding any constructors, methods, augmented methods, or fields.
214-
if (javaClassName == null) {
215-
throw new IllegalArgumentException("invalid object definition: expected a class name [" + line + "]");
304+
if (parseType == null) {
305+
throw new IllegalArgumentException("invalid definition: expected one of ['class', 'static'] [" + line + "]");
216306
}
217307

218308
// Handle the case for a constructor definition.
@@ -221,7 +311,7 @@ public static Whitelist loadFromResourceFiles(Class<?> resource, String... filep
221311
// Ensure the final token of the line is ')'.
222312
if (line.endsWith(")") == false) {
223313
throw new IllegalArgumentException(
224-
"invalid constructor definition: expected a closing parenthesis [" + line + "]");
314+
"invalid constructor definition: expected a closing parenthesis [" + line + "]");
225315
}
226316

227317
// Parse the constructor parameters.
@@ -234,34 +324,34 @@ public static Whitelist loadFromResourceFiles(Class<?> resource, String... filep
234324

235325
whitelistConstructors.add(new WhitelistConstructor(origin, Arrays.asList(tokens)));
236326

237-
// Handle the case for a method or augmented method definition.
238-
// Expects the following format: ID ID? ID '(' ( ID ( ',' ID )* )? ')' '\n'
327+
// Handle the case for a method or augmented method definition.
328+
// Expects the following format: ID ID? ID '(' ( ID ( ',' ID )* )? ')' '\n'
239329
} else if (line.contains("(")) {
240330
// Ensure the final token of the line is ')'.
241331
if (line.endsWith(")") == false) {
242332
throw new IllegalArgumentException(
243-
"invalid method definition: expected a closing parenthesis [" + line + "]");
333+
"invalid method definition: expected a closing parenthesis [" + line + "]");
244334
}
245335

246336
// Parse the tokens prior to the method parameters.
247337
int parameterIndex = line.indexOf('(');
248-
String[] tokens = line.trim().substring(0, parameterIndex).split("\\s+");
338+
String[] tokens = line.substring(0, parameterIndex).trim().split("\\s+");
249339

250-
String javaMethodName;
340+
String methodName;
251341
String javaAugmentedClassName;
252342

253343
// Based on the number of tokens, look up the Java method name and if provided the Java augmented class.
254344
if (tokens.length == 2) {
255-
javaMethodName = tokens[1];
345+
methodName = tokens[1];
256346
javaAugmentedClassName = null;
257347
} else if (tokens.length == 3) {
258-
javaMethodName = tokens[2];
348+
methodName = tokens[2];
259349
javaAugmentedClassName = tokens[1];
260350
} else {
261351
throw new IllegalArgumentException("invalid method definition: unexpected format [" + line + "]");
262352
}
263353

264-
String painlessReturnTypeName = tokens[0];
354+
String returnCanonicalTypeName = tokens[0];
265355

266356
// Parse the method parameters.
267357
tokens = line.substring(parameterIndex + 1, line.length() - 1).replaceAll("\\s+", "").split(",");
@@ -271,11 +361,11 @@ public static Whitelist loadFromResourceFiles(Class<?> resource, String... filep
271361
tokens = new String[0];
272362
}
273363

274-
whitelistMethods.add(new WhitelistMethod(origin, javaAugmentedClassName, javaMethodName,
275-
painlessReturnTypeName, Arrays.asList(tokens)));
364+
whitelistMethods.add(new WhitelistMethod(origin, javaAugmentedClassName, methodName,
365+
returnCanonicalTypeName, Arrays.asList(tokens)));
276366

277-
// Handle the case for a field definition.
278-
// Expects the following format: ID ID '\n'
367+
// Handle the case for a field definition.
368+
// Expects the following format: ID ID '\n'
279369
} else {
280370
// Parse the field tokens.
281371
String[] tokens = line.split("\\s+");
@@ -287,20 +377,23 @@ public static Whitelist loadFromResourceFiles(Class<?> resource, String... filep
287377

288378
whitelistFields.add(new WhitelistField(origin, tokens[1], tokens[0]));
289379
}
380+
} else {
381+
throw new IllegalArgumentException("invalid definition: unable to parse line [" + line + "]");
290382
}
291383
}
292384

293385
// Ensure all classes end with a '}' token before the end of the file.
294386
if (javaClassName != null) {
295-
throw new IllegalArgumentException("invalid class definition: expected closing bracket");
387+
throw new IllegalArgumentException("invalid definition: expected closing bracket");
296388
}
297389
} catch (Exception exception) {
298390
throw new RuntimeException("error in [" + filepath + "] at line [" + number + "]", exception);
299391
}
300392
}
393+
301394
ClassLoader loader = AccessController.doPrivileged((PrivilegedAction<ClassLoader>)resource::getClassLoader);
302395

303-
return new Whitelist(loader, whitelistClasses);
396+
return new Whitelist(loader, whitelistClasses, whitelistBindings);
304397
}
305398

306399
private WhitelistLoader() {}

0 commit comments

Comments
 (0)