Painless: Add Bindings (#33042)
Add bindings that allow some specialized methods to store permanent state between script executions.
This commit is contained in:
parent
6daf8115d6
commit
b52818ec6f
|
@ -61,9 +61,12 @@ public final class Whitelist {
|
|||
/** The {@link List} of all the whitelisted Painless classes. */
|
||||
public final List<WhitelistClass> whitelistClasses;
|
||||
|
||||
public final List<WhitelistBinding> whitelistBindings;
|
||||
|
||||
/** Standard constructor. All values must be not {@code null}. */
|
||||
public Whitelist(ClassLoader classLoader, List<WhitelistClass> whitelistClasses) {
|
||||
public Whitelist(ClassLoader classLoader, List<WhitelistClass> whitelistClasses, List<WhitelistBinding> whitelistBindings) {
|
||||
this.classLoader = Objects.requireNonNull(classLoader);
|
||||
this.whitelistClasses = Collections.unmodifiableList(Objects.requireNonNull(whitelistClasses));
|
||||
this.whitelistBindings = Collections.unmodifiableList(Objects.requireNonNull(whitelistBindings));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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.spi;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A binding represents a method call that stores state. Each binding class must have exactly one
|
||||
* public constructor and one public method excluding those inherited directly from {@link Object}.
|
||||
* The canonical type name parameters provided must match those of the constructor and method combined.
|
||||
* The constructor for a binding class will be called when the binding method is called for the first
|
||||
* time at which point state may be stored for the arguments passed into the constructor. The method
|
||||
* for a binding class will be called each time the binding method is called and may use the previously
|
||||
* stored state.
|
||||
*/
|
||||
public class WhitelistBinding {
|
||||
|
||||
/** Information about where this constructor was whitelisted from. */
|
||||
public final String origin;
|
||||
|
||||
/** The Java class name this binding represents. */
|
||||
public final String targetJavaClassName;
|
||||
|
||||
/** The method name for this binding. */
|
||||
public final String methodName;
|
||||
|
||||
/**
|
||||
* The canonical type name for the return type.
|
||||
*/
|
||||
public final String returnCanonicalTypeName;
|
||||
|
||||
/**
|
||||
* A {@link List} of {@link String}s that are the Painless type names for the parameters of the
|
||||
* constructor which can be used to look up the Java constructor through reflection.
|
||||
*/
|
||||
public final List<String> canonicalTypeNameParameters;
|
||||
|
||||
/** Standard constructor. All values must be not {@code null}. */
|
||||
public WhitelistBinding(String origin, String targetJavaClassName,
|
||||
String methodName, String returnCanonicalTypeName, List<String> canonicalTypeNameParameters) {
|
||||
|
||||
this.origin = Objects.requireNonNull(origin);
|
||||
this.targetJavaClassName = Objects.requireNonNull(targetJavaClassName);
|
||||
|
||||
this.methodName = Objects.requireNonNull(methodName);
|
||||
this.returnCanonicalTypeName = Objects.requireNonNull(returnCanonicalTypeName);
|
||||
this.canonicalTypeNameParameters = Objects.requireNonNull(canonicalTypeNameParameters);
|
||||
}
|
||||
}
|
|
@ -62,9 +62,8 @@ public final class WhitelistClass {
|
|||
|
||||
/** Standard constructor. All values must be not {@code null}. */
|
||||
public WhitelistClass(String origin, String javaClassName, boolean noImport,
|
||||
List<WhitelistConstructor> whitelistConstructors,
|
||||
List<WhitelistMethod> whitelistMethods,
|
||||
List<WhitelistField> whitelistFields) {
|
||||
List<WhitelistConstructor> whitelistConstructors, List<WhitelistMethod> whitelistMethods, List<WhitelistField> whitelistFields)
|
||||
{
|
||||
|
||||
this.origin = Objects.requireNonNull(origin);
|
||||
this.javaClassName = Objects.requireNonNull(javaClassName);
|
||||
|
|
|
@ -133,6 +133,7 @@ public final class WhitelistLoader {
|
|||
*/
|
||||
public static Whitelist loadFromResourceFiles(Class<?> resource, String... filepaths) {
|
||||
List<WhitelistClass> whitelistClasses = new ArrayList<>();
|
||||
List<WhitelistBinding> whitelistBindings = new ArrayList<>();
|
||||
|
||||
// Execute a single pass through the whitelist text files. This will gather all the
|
||||
// constructors, methods, augmented methods, and fields for each whitelisted class.
|
||||
|
@ -141,8 +142,9 @@ public final class WhitelistLoader {
|
|||
int number = -1;
|
||||
|
||||
try (LineNumberReader reader = new LineNumberReader(
|
||||
new InputStreamReader(resource.getResourceAsStream(filepath), StandardCharsets.UTF_8))) {
|
||||
new InputStreamReader(resource.getResourceAsStream(filepath), StandardCharsets.UTF_8))) {
|
||||
|
||||
String parseType = null;
|
||||
String whitelistClassOrigin = null;
|
||||
String javaClassName = null;
|
||||
boolean noImport = false;
|
||||
|
@ -165,7 +167,11 @@ public final class WhitelistLoader {
|
|||
// Ensure the final token of the line is '{'.
|
||||
if (line.endsWith("{") == false) {
|
||||
throw new IllegalArgumentException(
|
||||
"invalid class definition: failed to parse class opening bracket [" + line + "]");
|
||||
"invalid class definition: failed to parse class opening bracket [" + line + "]");
|
||||
}
|
||||
|
||||
if (parseType != null) {
|
||||
throw new IllegalArgumentException("invalid definition: cannot embed class definition [" + line + "]");
|
||||
}
|
||||
|
||||
// Parse the Java class name.
|
||||
|
@ -178,6 +184,7 @@ public final class WhitelistLoader {
|
|||
throw new IllegalArgumentException("invalid class definition: failed to parse class name [" + line + "]");
|
||||
}
|
||||
|
||||
parseType = "class";
|
||||
whitelistClassOrigin = "[" + filepath + "]:[" + number + "]";
|
||||
javaClassName = tokens[0];
|
||||
|
||||
|
@ -185,34 +192,117 @@ public final class WhitelistLoader {
|
|||
whitelistConstructors = new ArrayList<>();
|
||||
whitelistMethods = new ArrayList<>();
|
||||
whitelistFields = new ArrayList<>();
|
||||
|
||||
// Handle the end of a class, by creating a new WhitelistClass with all the previously gathered
|
||||
// constructors, methods, augmented methods, and fields, and adding it to the list of whitelisted classes.
|
||||
// Expects the following format: '}' '\n'
|
||||
} else if (line.equals("}")) {
|
||||
if (javaClassName == null) {
|
||||
throw new IllegalArgumentException("invalid class definition: extraneous closing bracket");
|
||||
} else if (line.startsWith("static ")) {
|
||||
// Ensure the final token of the line is '{'.
|
||||
if (line.endsWith("{") == false) {
|
||||
throw new IllegalArgumentException(
|
||||
"invalid static definition: failed to parse static opening bracket [" + line + "]");
|
||||
}
|
||||
|
||||
whitelistClasses.add(new WhitelistClass(whitelistClassOrigin, javaClassName, noImport,
|
||||
whitelistConstructors, whitelistMethods, whitelistFields));
|
||||
if (parseType != null) {
|
||||
throw new IllegalArgumentException("invalid definition: cannot embed static definition [" + line + "]");
|
||||
}
|
||||
|
||||
// Set all the variables to null to ensure a new class definition is found before other parsable values.
|
||||
whitelistClassOrigin = null;
|
||||
javaClassName = null;
|
||||
noImport = false;
|
||||
whitelistConstructors = null;
|
||||
whitelistMethods = null;
|
||||
whitelistFields = null;
|
||||
parseType = "static";
|
||||
|
||||
// Handle all other valid cases.
|
||||
} else {
|
||||
// Handle the end of a definition and reset all previously gathered values.
|
||||
// Expects the following format: '}' '\n'
|
||||
} else if (line.equals("}")) {
|
||||
if (parseType == null) {
|
||||
throw new IllegalArgumentException("invalid definition: extraneous closing bracket");
|
||||
}
|
||||
|
||||
// Create a new WhitelistClass with all the previously gathered constructors, methods,
|
||||
// augmented methods, and fields, and add it to the list of whitelisted classes.
|
||||
if ("class".equals(parseType)) {
|
||||
whitelistClasses.add(new WhitelistClass(whitelistClassOrigin, javaClassName, noImport,
|
||||
whitelistConstructors, whitelistMethods, whitelistFields));
|
||||
|
||||
whitelistClassOrigin = null;
|
||||
javaClassName = null;
|
||||
noImport = false;
|
||||
whitelistConstructors = null;
|
||||
whitelistMethods = null;
|
||||
whitelistFields = null;
|
||||
}
|
||||
|
||||
// Reset the parseType.
|
||||
parseType = null;
|
||||
|
||||
// Handle static definition types.
|
||||
// Expects the following format: ID ID '(' ( ID ( ',' ID )* )? ')' 'bound_to' ID '\n'
|
||||
} else if ("static".equals(parseType)) {
|
||||
// Mark the origin of this parsable object.
|
||||
String origin = "[" + filepath + "]:[" + number + "]";
|
||||
|
||||
// Parse the tokens prior to the method parameters.
|
||||
int parameterStartIndex = line.indexOf('(');
|
||||
|
||||
if (parameterStartIndex == -1) {
|
||||
throw new IllegalArgumentException(
|
||||
"illegal static definition: start of method parameters not found [" + line + "]");
|
||||
}
|
||||
|
||||
String[] tokens = line.substring(0, parameterStartIndex).trim().split("\\s+");
|
||||
|
||||
String methodName;
|
||||
|
||||
// Based on the number of tokens, look up the Java method name.
|
||||
if (tokens.length == 2) {
|
||||
methodName = tokens[1];
|
||||
} else {
|
||||
throw new IllegalArgumentException("invalid method definition: unexpected format [" + line + "]");
|
||||
}
|
||||
|
||||
String returnCanonicalTypeName = tokens[0];
|
||||
|
||||
// Parse the method parameters.
|
||||
int parameterEndIndex = line.indexOf(')');
|
||||
|
||||
if (parameterEndIndex == -1) {
|
||||
throw new IllegalArgumentException(
|
||||
"illegal static definition: end of method parameters not found [" + line + "]");
|
||||
}
|
||||
|
||||
String[] canonicalTypeNameParameters =
|
||||
line.substring(parameterStartIndex + 1, parameterEndIndex).replaceAll("\\s+", "").split(",");
|
||||
|
||||
// Handle the case for a method with no parameters.
|
||||
if ("".equals(canonicalTypeNameParameters[0])) {
|
||||
canonicalTypeNameParameters = new String[0];
|
||||
}
|
||||
|
||||
// Parse the static type and class.
|
||||
tokens = line.substring(parameterEndIndex + 1).trim().split("\\s+");
|
||||
|
||||
String staticType;
|
||||
String targetJavaClassName;
|
||||
|
||||
// Based on the number of tokens, look up the type and class.
|
||||
if (tokens.length == 2) {
|
||||
staticType = tokens[0];
|
||||
targetJavaClassName = tokens[1];
|
||||
} else {
|
||||
throw new IllegalArgumentException("invalid static definition: unexpected format [" + line + "]");
|
||||
}
|
||||
|
||||
// Check the static type is valid.
|
||||
if ("bound_to".equals(staticType) == false) {
|
||||
throw new IllegalArgumentException(
|
||||
"invalid static definition: unexpected static type [" + staticType + "] [" + line + "]");
|
||||
}
|
||||
|
||||
whitelistBindings.add(new WhitelistBinding(origin, targetJavaClassName,
|
||||
methodName, returnCanonicalTypeName, Arrays.asList(canonicalTypeNameParameters)));
|
||||
|
||||
// Handle class definition types.
|
||||
} else if ("class".equals(parseType)) {
|
||||
// Mark the origin of this parsable object.
|
||||
String origin = "[" + filepath + "]:[" + number + "]";
|
||||
|
||||
// Ensure we have a defined class before adding any constructors, methods, augmented methods, or fields.
|
||||
if (javaClassName == null) {
|
||||
throw new IllegalArgumentException("invalid object definition: expected a class name [" + line + "]");
|
||||
if (parseType == null) {
|
||||
throw new IllegalArgumentException("invalid definition: expected one of ['class', 'static'] [" + line + "]");
|
||||
}
|
||||
|
||||
// Handle the case for a constructor definition.
|
||||
|
@ -221,7 +311,7 @@ public final class WhitelistLoader {
|
|||
// Ensure the final token of the line is ')'.
|
||||
if (line.endsWith(")") == false) {
|
||||
throw new IllegalArgumentException(
|
||||
"invalid constructor definition: expected a closing parenthesis [" + line + "]");
|
||||
"invalid constructor definition: expected a closing parenthesis [" + line + "]");
|
||||
}
|
||||
|
||||
// Parse the constructor parameters.
|
||||
|
@ -234,34 +324,34 @@ public final class WhitelistLoader {
|
|||
|
||||
whitelistConstructors.add(new WhitelistConstructor(origin, Arrays.asList(tokens)));
|
||||
|
||||
// Handle the case for a method or augmented method definition.
|
||||
// Expects the following format: ID ID? ID '(' ( ID ( ',' ID )* )? ')' '\n'
|
||||
// Handle the case for a method or augmented method definition.
|
||||
// Expects the following format: ID ID? ID '(' ( ID ( ',' ID )* )? ')' '\n'
|
||||
} else if (line.contains("(")) {
|
||||
// Ensure the final token of the line is ')'.
|
||||
if (line.endsWith(")") == false) {
|
||||
throw new IllegalArgumentException(
|
||||
"invalid method definition: expected a closing parenthesis [" + line + "]");
|
||||
"invalid method definition: expected a closing parenthesis [" + line + "]");
|
||||
}
|
||||
|
||||
// Parse the tokens prior to the method parameters.
|
||||
int parameterIndex = line.indexOf('(');
|
||||
String[] tokens = line.trim().substring(0, parameterIndex).split("\\s+");
|
||||
String[] tokens = line.substring(0, parameterIndex).trim().split("\\s+");
|
||||
|
||||
String javaMethodName;
|
||||
String methodName;
|
||||
String javaAugmentedClassName;
|
||||
|
||||
// Based on the number of tokens, look up the Java method name and if provided the Java augmented class.
|
||||
if (tokens.length == 2) {
|
||||
javaMethodName = tokens[1];
|
||||
methodName = tokens[1];
|
||||
javaAugmentedClassName = null;
|
||||
} else if (tokens.length == 3) {
|
||||
javaMethodName = tokens[2];
|
||||
methodName = tokens[2];
|
||||
javaAugmentedClassName = tokens[1];
|
||||
} else {
|
||||
throw new IllegalArgumentException("invalid method definition: unexpected format [" + line + "]");
|
||||
}
|
||||
|
||||
String painlessReturnTypeName = tokens[0];
|
||||
String returnCanonicalTypeName = tokens[0];
|
||||
|
||||
// Parse the method parameters.
|
||||
tokens = line.substring(parameterIndex + 1, line.length() - 1).replaceAll("\\s+", "").split(",");
|
||||
|
@ -271,11 +361,11 @@ public final class WhitelistLoader {
|
|||
tokens = new String[0];
|
||||
}
|
||||
|
||||
whitelistMethods.add(new WhitelistMethod(origin, javaAugmentedClassName, javaMethodName,
|
||||
painlessReturnTypeName, Arrays.asList(tokens)));
|
||||
whitelistMethods.add(new WhitelistMethod(origin, javaAugmentedClassName, methodName,
|
||||
returnCanonicalTypeName, Arrays.asList(tokens)));
|
||||
|
||||
// Handle the case for a field definition.
|
||||
// Expects the following format: ID ID '\n'
|
||||
// Handle the case for a field definition.
|
||||
// Expects the following format: ID ID '\n'
|
||||
} else {
|
||||
// Parse the field tokens.
|
||||
String[] tokens = line.split("\\s+");
|
||||
|
@ -287,20 +377,23 @@ public final class WhitelistLoader {
|
|||
|
||||
whitelistFields.add(new WhitelistField(origin, tokens[1], tokens[0]));
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("invalid definition: unable to parse line [" + line + "]");
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure all classes end with a '}' token before the end of the file.
|
||||
if (javaClassName != null) {
|
||||
throw new IllegalArgumentException("invalid class definition: expected closing bracket");
|
||||
throw new IllegalArgumentException("invalid definition: expected closing bracket");
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
throw new RuntimeException("error in [" + filepath + "] at line [" + number + "]", exception);
|
||||
}
|
||||
}
|
||||
|
||||
ClassLoader loader = AccessController.doPrivileged((PrivilegedAction<ClassLoader>)resource::getClassLoader);
|
||||
|
||||
return new Whitelist(loader, whitelistClasses);
|
||||
return new Whitelist(loader, whitelistClasses, whitelistBindings);
|
||||
}
|
||||
|
||||
private WhitelistLoader() {}
|
||||
|
|
|
@ -67,7 +67,8 @@ public class WhitelistMethod {
|
|||
* is augmented as described in the class documentation.
|
||||
*/
|
||||
public WhitelistMethod(String origin, String augmentedCanonicalClassName, String methodName,
|
||||
String returnCanonicalTypeName, List<String> canonicalTypeNameParameters) {
|
||||
String returnCanonicalTypeName, List<String> canonicalTypeNameParameters) {
|
||||
|
||||
this.origin = Objects.requireNonNull(origin);
|
||||
this.augmentedCanonicalClassName = augmentedCanonicalClassName;
|
||||
this.methodName = methodName;
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
public class BindingTest {
|
||||
public int state;
|
||||
|
||||
public BindingTest(int state0, int state1) {
|
||||
this.state = state0 + state1;
|
||||
}
|
||||
|
||||
public int testAddWithState(int stateless) {
|
||||
return stateless + state;
|
||||
}
|
||||
}
|
|
@ -31,6 +31,7 @@ import java.util.Map;
|
|||
public class Globals {
|
||||
private final Map<String,SFunction> syntheticMethods = new HashMap<>();
|
||||
private final Map<String,Constant> constantInitializers = new HashMap<>();
|
||||
private final Map<String,Class<?>> bindings = new HashMap<>();
|
||||
private final BitSet statements;
|
||||
|
||||
/** Create a new Globals from the set of statement boundaries */
|
||||
|
@ -54,7 +55,15 @@ public class Globals {
|
|||
throw new IllegalStateException("constant initializer: " + constant.name + " already exists");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Adds a new binding to be written as a local variable */
|
||||
public String addBinding(Class<?> type) {
|
||||
String name = "$binding$" + bindings.size();
|
||||
bindings.put(name, type);
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
/** Returns the current synthetic methods */
|
||||
public Map<String,SFunction> getSyntheticMethods() {
|
||||
return syntheticMethods;
|
||||
|
@ -64,7 +73,12 @@ public class Globals {
|
|||
public Map<String,Constant> getConstantInitializers() {
|
||||
return constantInitializers;
|
||||
}
|
||||
|
||||
|
||||
/** Returns the current bindings */
|
||||
public Map<String,Class<?>> getBindings() {
|
||||
return bindings;
|
||||
}
|
||||
|
||||
/** Returns the set of statement boundaries */
|
||||
public BitSet getStatements() {
|
||||
return statements;
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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.lookup;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
public class PainlessBinding {
|
||||
|
||||
public final Constructor<?> javaConstructor;
|
||||
public final Method javaMethod;
|
||||
|
||||
public final Class<?> returnType;
|
||||
public final List<Class<?>> typeParameters;
|
||||
|
||||
PainlessBinding(Constructor<?> javaConstructor, Method javaMethod, Class<?> returnType, List<Class<?>> typeParameters) {
|
||||
this.javaConstructor = javaConstructor;
|
||||
this.javaMethod = javaMethod;
|
||||
|
||||
this.returnType = returnType;
|
||||
this.typeParameters = typeParameters;
|
||||
}
|
||||
}
|
|
@ -24,6 +24,7 @@ import java.util.Collections;
|
|||
import java.util.Map;
|
||||
|
||||
public final class PainlessClass {
|
||||
|
||||
public final Map<String, PainlessConstructor> constructors;
|
||||
|
||||
public final Map<String, PainlessMethod> staticMethods;
|
||||
|
|
|
@ -24,6 +24,7 @@ import java.util.HashMap;
|
|||
import java.util.Map;
|
||||
|
||||
final class PainlessClassBuilder {
|
||||
|
||||
final Map<String, PainlessConstructor> constructors;
|
||||
|
||||
final Map<String, PainlessMethod> staticMethods;
|
||||
|
|
|
@ -25,6 +25,7 @@ import java.lang.reflect.Constructor;
|
|||
import java.util.List;
|
||||
|
||||
public class PainlessConstructor {
|
||||
|
||||
public final Constructor<?> javaConstructor;
|
||||
public final List<Class<?>> typeParameters;
|
||||
public final MethodHandle methodHandle;
|
||||
|
|
|
@ -23,6 +23,7 @@ import java.lang.invoke.MethodHandle;
|
|||
import java.lang.reflect.Field;
|
||||
|
||||
public final class PainlessField {
|
||||
|
||||
public final Field javaField;
|
||||
public final Class<?> typeParameter;
|
||||
|
||||
|
|
|
@ -37,12 +37,17 @@ public final class PainlessLookup {
|
|||
private final Map<String, Class<?>> canonicalClassNamesToClasses;
|
||||
private final Map<Class<?>, PainlessClass> classesToPainlessClasses;
|
||||
|
||||
PainlessLookup(Map<String, Class<?>> canonicalClassNamesToClasses, Map<Class<?>, PainlessClass> classesToPainlessClasses) {
|
||||
private final Map<String, PainlessBinding> painlessMethodKeysToPainlessBindings;
|
||||
|
||||
PainlessLookup(Map<String, Class<?>> canonicalClassNamesToClasses, Map<Class<?>, PainlessClass> classesToPainlessClasses,
|
||||
Map<String, PainlessBinding> painlessMethodKeysToPainlessBindings) {
|
||||
Objects.requireNonNull(canonicalClassNamesToClasses);
|
||||
Objects.requireNonNull(classesToPainlessClasses);
|
||||
|
||||
this.canonicalClassNamesToClasses = Collections.unmodifiableMap(canonicalClassNamesToClasses);
|
||||
this.classesToPainlessClasses = Collections.unmodifiableMap(classesToPainlessClasses);
|
||||
|
||||
this.painlessMethodKeysToPainlessBindings = Collections.unmodifiableMap(painlessMethodKeysToPainlessBindings);
|
||||
}
|
||||
|
||||
public boolean isValidCanonicalClassName(String canonicalClassName) {
|
||||
|
@ -162,6 +167,14 @@ public final class PainlessLookup {
|
|||
return painlessField;
|
||||
}
|
||||
|
||||
public PainlessBinding lookupPainlessBinding(String methodName, int arity) {
|
||||
Objects.requireNonNull(methodName);
|
||||
|
||||
String painlessMethodKey = buildPainlessMethodKey(methodName, arity);
|
||||
|
||||
return painlessMethodKeysToPainlessBindings.get(painlessMethodKey);
|
||||
}
|
||||
|
||||
public PainlessMethod lookupFunctionalInterfacePainlessMethod(Class<?> targetClass) {
|
||||
PainlessClass targetPainlessClass = classesToPainlessClasses.get(targetClass);
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
package org.elasticsearch.painless.lookup;
|
||||
|
||||
import org.elasticsearch.painless.spi.Whitelist;
|
||||
import org.elasticsearch.painless.spi.WhitelistBinding;
|
||||
import org.elasticsearch.painless.spi.WhitelistClass;
|
||||
import org.elasticsearch.painless.spi.WhitelistConstructor;
|
||||
import org.elasticsearch.painless.spi.WhitelistField;
|
||||
|
@ -52,11 +53,11 @@ public final class PainlessLookupBuilder {
|
|||
|
||||
private static class PainlessConstructorCacheKey {
|
||||
|
||||
private final Class<?> targetType;
|
||||
private final Class<?> targetClass;
|
||||
private final List<Class<?>> typeParameters;
|
||||
|
||||
private PainlessConstructorCacheKey(Class<?> targetType, List<Class<?>> typeParameters) {
|
||||
this.targetType = targetType;
|
||||
private PainlessConstructorCacheKey(Class<?> targetClass, List<Class<?>> typeParameters) {
|
||||
this.targetClass = targetClass;
|
||||
this.typeParameters = Collections.unmodifiableList(typeParameters);
|
||||
}
|
||||
|
||||
|
@ -72,25 +73,27 @@ public final class PainlessLookupBuilder {
|
|||
|
||||
PainlessConstructorCacheKey that = (PainlessConstructorCacheKey)object;
|
||||
|
||||
return Objects.equals(targetType, that.targetType) &&
|
||||
return Objects.equals(targetClass, that.targetClass) &&
|
||||
Objects.equals(typeParameters, that.typeParameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(targetType, typeParameters);
|
||||
return Objects.hash(targetClass, typeParameters);
|
||||
}
|
||||
}
|
||||
|
||||
private static class PainlessMethodCacheKey {
|
||||
|
||||
private final Class<?> targetType;
|
||||
private final Class<?> targetClass;
|
||||
private final String methodName;
|
||||
private final Class<?> returnType;
|
||||
private final List<Class<?>> typeParameters;
|
||||
|
||||
private PainlessMethodCacheKey(Class<?> targetType, String methodName, List<Class<?>> typeParameters) {
|
||||
this.targetType = targetType;
|
||||
private PainlessMethodCacheKey(Class<?> targetClass, String methodName, Class<?> returnType, List<Class<?>> typeParameters) {
|
||||
this.targetClass = targetClass;
|
||||
this.methodName = methodName;
|
||||
this.returnType = returnType;
|
||||
this.typeParameters = Collections.unmodifiableList(typeParameters);
|
||||
}
|
||||
|
||||
|
@ -106,25 +109,26 @@ public final class PainlessLookupBuilder {
|
|||
|
||||
PainlessMethodCacheKey that = (PainlessMethodCacheKey)object;
|
||||
|
||||
return Objects.equals(targetType, that.targetType) &&
|
||||
return Objects.equals(targetClass, that.targetClass) &&
|
||||
Objects.equals(methodName, that.methodName) &&
|
||||
Objects.equals(returnType, that.returnType) &&
|
||||
Objects.equals(typeParameters, that.typeParameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(targetType, methodName, typeParameters);
|
||||
return Objects.hash(targetClass, methodName, returnType, typeParameters);
|
||||
}
|
||||
}
|
||||
|
||||
private static class PainlessFieldCacheKey {
|
||||
|
||||
private final Class<?> targetType;
|
||||
private final Class<?> targetClass;
|
||||
private final String fieldName;
|
||||
private final Class<?> typeParameter;
|
||||
|
||||
private PainlessFieldCacheKey(Class<?> targetType, String fieldName, Class<?> typeParameter) {
|
||||
this.targetType = targetType;
|
||||
private PainlessFieldCacheKey(Class<?> targetClass, String fieldName, Class<?> typeParameter) {
|
||||
this.targetClass = targetClass;
|
||||
this.fieldName = fieldName;
|
||||
this.typeParameter = typeParameter;
|
||||
}
|
||||
|
@ -141,20 +145,61 @@ public final class PainlessLookupBuilder {
|
|||
|
||||
PainlessFieldCacheKey that = (PainlessFieldCacheKey) object;
|
||||
|
||||
return Objects.equals(targetType, that.targetType) &&
|
||||
return Objects.equals(targetClass, that.targetClass) &&
|
||||
Objects.equals(fieldName, that.fieldName) &&
|
||||
Objects.equals(typeParameter, that.typeParameter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(targetType, fieldName, typeParameter);
|
||||
return Objects.hash(targetClass, fieldName, typeParameter);
|
||||
}
|
||||
}
|
||||
|
||||
private static final Map<PainlessConstructorCacheKey, PainlessConstructor> painlessConstuctorCache = new HashMap<>();
|
||||
private static final Map<PainlessMethodCacheKey, PainlessMethod> painlessMethodCache = new HashMap<>();
|
||||
private static final Map<PainlessFieldCacheKey, PainlessField> painlessFieldCache = new HashMap<>();
|
||||
private static class PainlessBindingCacheKey {
|
||||
|
||||
private final Class<?> targetClass;
|
||||
private final String methodName;
|
||||
private final Class<?> methodReturnType;
|
||||
private final List<Class<?>> methodTypeParameters;
|
||||
|
||||
private PainlessBindingCacheKey(Class<?> targetClass,
|
||||
String methodName, Class<?> returnType, List<Class<?>> typeParameters) {
|
||||
|
||||
this.targetClass = targetClass;
|
||||
this.methodName = methodName;
|
||||
this.methodReturnType = returnType;
|
||||
this.methodTypeParameters = Collections.unmodifiableList(typeParameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
if (this == object) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (object == null || getClass() != object.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
PainlessBindingCacheKey that = (PainlessBindingCacheKey)object;
|
||||
|
||||
return Objects.equals(targetClass, that.targetClass) &&
|
||||
Objects.equals(methodName, that.methodName) &&
|
||||
Objects.equals(methodReturnType, that.methodReturnType) &&
|
||||
Objects.equals(methodTypeParameters, that.methodTypeParameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(targetClass, methodName, methodReturnType, methodTypeParameters);
|
||||
}
|
||||
}
|
||||
|
||||
private static final Map<PainlessConstructorCacheKey, PainlessConstructor> painlessConstructorCache = new HashMap<>();
|
||||
private static final Map<PainlessMethodCacheKey, PainlessMethod> painlessMethodCache = new HashMap<>();
|
||||
private static final Map<PainlessFieldCacheKey, PainlessField> painlessFieldCache = new HashMap<>();
|
||||
private static final Map<PainlessBindingCacheKey, PainlessBinding> painlessBindingCache = new HashMap<>();
|
||||
|
||||
private static final Pattern CLASS_NAME_PATTERN = Pattern.compile("^[_a-zA-Z][._a-zA-Z0-9]*$");
|
||||
private static final Pattern METHOD_NAME_PATTERN = Pattern.compile("^[_a-zA-Z][_a-zA-Z0-9]*$");
|
||||
|
@ -197,6 +242,14 @@ public final class PainlessLookupBuilder {
|
|||
targetCanonicalClassName, whitelistField.fieldName, whitelistField.canonicalTypeNameParameter);
|
||||
}
|
||||
}
|
||||
|
||||
for (WhitelistBinding whitelistBinding : whitelist.whitelistBindings) {
|
||||
origin = whitelistBinding.origin;
|
||||
painlessLookupBuilder.addPainlessBinding(
|
||||
whitelist.classLoader, whitelistBinding.targetJavaClassName,
|
||||
whitelistBinding.methodName, whitelistBinding.returnCanonicalTypeName,
|
||||
whitelistBinding.canonicalTypeNameParameters);
|
||||
}
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
throw new IllegalArgumentException("error loading whitelist(s) " + origin, exception);
|
||||
|
@ -208,9 +261,13 @@ public final class PainlessLookupBuilder {
|
|||
private final Map<String, Class<?>> canonicalClassNamesToClasses;
|
||||
private final Map<Class<?>, PainlessClassBuilder> classesToPainlessClassBuilders;
|
||||
|
||||
private final Map<String, PainlessBinding> painlessMethodKeysToPainlessBindings;
|
||||
|
||||
public PainlessLookupBuilder() {
|
||||
canonicalClassNamesToClasses = new HashMap<>();
|
||||
classesToPainlessClassBuilders = new HashMap<>();
|
||||
|
||||
painlessMethodKeysToPainlessBindings = new HashMap<>();
|
||||
}
|
||||
|
||||
private Class<?> canonicalTypeNameToType(String canonicalTypeName) {
|
||||
|
@ -392,7 +449,7 @@ public final class PainlessLookupBuilder {
|
|||
|
||||
MethodType methodType = methodHandle.type();
|
||||
|
||||
painlessConstructor = painlessConstuctorCache.computeIfAbsent(
|
||||
painlessConstructor = painlessConstructorCache.computeIfAbsent(
|
||||
new PainlessConstructorCacheKey(targetClass, typeParameters),
|
||||
key -> new PainlessConstructor(javaConstructor, typeParameters, methodHandle, methodType)
|
||||
);
|
||||
|
@ -439,7 +496,7 @@ public final class PainlessLookupBuilder {
|
|||
Class<?> typeParameter = canonicalTypeNameToType(canonicalTypeNameParameter);
|
||||
|
||||
if (typeParameter == null) {
|
||||
throw new IllegalArgumentException("parameter type [" + canonicalTypeNameParameter + "] not found for method " +
|
||||
throw new IllegalArgumentException("type parameter [" + canonicalTypeNameParameter + "] not found for method " +
|
||||
"[[" + targetCanonicalClassName + "], [" + methodName + "], " + canonicalTypeNameParameters + "]");
|
||||
}
|
||||
|
||||
|
@ -449,7 +506,7 @@ public final class PainlessLookupBuilder {
|
|||
Class<?> returnType = canonicalTypeNameToType(returnCanonicalTypeName);
|
||||
|
||||
if (returnType == null) {
|
||||
throw new IllegalArgumentException("parameter type [" + returnCanonicalTypeName + "] not found for method " +
|
||||
throw new IllegalArgumentException("return type [" + returnCanonicalTypeName + "] not found for method " +
|
||||
"[[" + targetCanonicalClassName + "], [" + methodName + "], " + canonicalTypeNameParameters + "]");
|
||||
}
|
||||
|
||||
|
@ -548,7 +605,7 @@ public final class PainlessLookupBuilder {
|
|||
MethodType methodType = methodHandle.type();
|
||||
|
||||
painlessMethod = painlessMethodCache.computeIfAbsent(
|
||||
new PainlessMethodCacheKey(targetClass, methodName, typeParameters),
|
||||
new PainlessMethodCacheKey(targetClass, methodName, returnType, typeParameters),
|
||||
key -> new PainlessMethod(javaMethod, targetClass, returnType, typeParameters, methodHandle, methodType));
|
||||
|
||||
painlessClassBuilder.staticMethods.put(painlessMethodKey, painlessMethod);
|
||||
|
@ -588,7 +645,7 @@ public final class PainlessLookupBuilder {
|
|||
MethodType methodType = methodHandle.type();
|
||||
|
||||
painlessMethod = painlessMethodCache.computeIfAbsent(
|
||||
new PainlessMethodCacheKey(targetClass, methodName, typeParameters),
|
||||
new PainlessMethodCacheKey(targetClass, methodName, returnType, typeParameters),
|
||||
key -> new PainlessMethod(javaMethod, targetClass, returnType, typeParameters, methodHandle, methodType));
|
||||
|
||||
painlessClassBuilder.methods.put(painlessMethodKey, painlessMethod);
|
||||
|
@ -731,6 +788,183 @@ public final class PainlessLookupBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
public void addPainlessBinding(ClassLoader classLoader, String targetJavaClassName,
|
||||
String methodName, String returnCanonicalTypeName, List<String> canonicalTypeNameParameters) {
|
||||
|
||||
Objects.requireNonNull(classLoader);
|
||||
Objects.requireNonNull(targetJavaClassName);
|
||||
Objects.requireNonNull(methodName);
|
||||
Objects.requireNonNull(returnCanonicalTypeName);
|
||||
Objects.requireNonNull(canonicalTypeNameParameters);
|
||||
|
||||
Class<?> targetClass;
|
||||
|
||||
try {
|
||||
targetClass = Class.forName(targetJavaClassName, true, classLoader);
|
||||
} catch (ClassNotFoundException cnfe) {
|
||||
throw new IllegalArgumentException("class [" + targetJavaClassName + "] not found", cnfe);
|
||||
}
|
||||
|
||||
String targetCanonicalClassName = typeToCanonicalTypeName(targetClass);
|
||||
List<Class<?>> typeParameters = new ArrayList<>(canonicalTypeNameParameters.size());
|
||||
|
||||
for (String canonicalTypeNameParameter : canonicalTypeNameParameters) {
|
||||
Class<?> typeParameter = canonicalTypeNameToType(canonicalTypeNameParameter);
|
||||
|
||||
if (typeParameter == null) {
|
||||
throw new IllegalArgumentException("type parameter [" + canonicalTypeNameParameter + "] not found for binding " +
|
||||
"[[" + targetCanonicalClassName + "], [" + methodName + "], " + canonicalTypeNameParameters + "]");
|
||||
}
|
||||
|
||||
typeParameters.add(typeParameter);
|
||||
}
|
||||
|
||||
Class<?> returnType = canonicalTypeNameToType(returnCanonicalTypeName);
|
||||
|
||||
if (returnType == null) {
|
||||
throw new IllegalArgumentException("return type [" + returnCanonicalTypeName + "] not found for binding " +
|
||||
"[[" + targetCanonicalClassName + "], [" + methodName + "], " + canonicalTypeNameParameters + "]");
|
||||
}
|
||||
|
||||
addPainlessBinding(targetClass, methodName, returnType, typeParameters);
|
||||
}
|
||||
|
||||
public void addPainlessBinding(Class<?> targetClass, String methodName, Class<?> returnType, List<Class<?>> typeParameters) {
|
||||
|
||||
Objects.requireNonNull(targetClass);
|
||||
Objects.requireNonNull(methodName);
|
||||
Objects.requireNonNull(returnType);
|
||||
Objects.requireNonNull(typeParameters);
|
||||
|
||||
if (targetClass == def.class) {
|
||||
throw new IllegalArgumentException("cannot add binding as reserved class [" + DEF_CLASS_NAME + "]");
|
||||
}
|
||||
|
||||
String targetCanonicalClassName = typeToCanonicalTypeName(targetClass);
|
||||
|
||||
Constructor<?>[] javaConstructors = targetClass.getConstructors();
|
||||
Constructor<?> javaConstructor = null;
|
||||
|
||||
for (Constructor<?> eachJavaConstructor : javaConstructors) {
|
||||
if (eachJavaConstructor.getDeclaringClass() == targetClass) {
|
||||
if (javaConstructor != null) {
|
||||
throw new IllegalArgumentException("binding [" + targetCanonicalClassName + "] cannot have multiple constructors");
|
||||
}
|
||||
|
||||
javaConstructor = eachJavaConstructor;
|
||||
}
|
||||
}
|
||||
|
||||
if (javaConstructor == null) {
|
||||
throw new IllegalArgumentException("binding [" + targetCanonicalClassName + "] must have exactly one constructor");
|
||||
}
|
||||
|
||||
int constructorTypeParametersSize = javaConstructor.getParameterCount();
|
||||
|
||||
for (int typeParameterIndex = 0; typeParameterIndex < constructorTypeParametersSize; ++typeParameterIndex) {
|
||||
Class<?> typeParameter = typeParameters.get(typeParameterIndex);
|
||||
|
||||
if (isValidType(typeParameter) == false) {
|
||||
throw new IllegalArgumentException("type parameter [" + typeToCanonicalTypeName(typeParameter) + "] not found " +
|
||||
"for binding [[" + targetCanonicalClassName + "], " + typesToCanonicalTypeNames(typeParameters) + "]");
|
||||
}
|
||||
|
||||
Class<?> javaTypeParameter = javaConstructor.getParameterTypes()[typeParameterIndex];
|
||||
|
||||
if (isValidType(javaTypeParameter) == false) {
|
||||
throw new IllegalArgumentException("type parameter [" + typeToCanonicalTypeName(typeParameter) + "] not found " +
|
||||
"for binding [[" + targetCanonicalClassName + "], " + typesToCanonicalTypeNames(typeParameters) + "]");
|
||||
}
|
||||
|
||||
if (javaTypeParameter != typeToJavaType(typeParameter)) {
|
||||
throw new IllegalArgumentException("type parameter [" + typeToCanonicalTypeName(javaTypeParameter) + "] " +
|
||||
"does not match the specified type parameter [" + typeToCanonicalTypeName(typeParameter) + "] " +
|
||||
"for binding [[" + targetClass.getCanonicalName() + "], " + typesToCanonicalTypeNames(typeParameters) + "]");
|
||||
}
|
||||
}
|
||||
|
||||
if (METHOD_NAME_PATTERN.matcher(methodName).matches() == false) {
|
||||
throw new IllegalArgumentException(
|
||||
"invalid method name [" + methodName + "] for binding [" + targetCanonicalClassName + "].");
|
||||
}
|
||||
|
||||
Method[] javaMethods = targetClass.getMethods();
|
||||
Method javaMethod = null;
|
||||
|
||||
for (Method eachJavaMethod : javaMethods) {
|
||||
if (eachJavaMethod.getDeclaringClass() == targetClass) {
|
||||
if (javaMethod != null) {
|
||||
throw new IllegalArgumentException("binding [" + targetCanonicalClassName + "] cannot have multiple methods");
|
||||
}
|
||||
|
||||
javaMethod = eachJavaMethod;
|
||||
}
|
||||
}
|
||||
|
||||
if (javaMethod == null) {
|
||||
throw new IllegalArgumentException("binding [" + targetCanonicalClassName + "] must have exactly one method");
|
||||
}
|
||||
|
||||
int methodTypeParametersSize = javaMethod.getParameterCount();
|
||||
|
||||
for (int typeParameterIndex = 0; typeParameterIndex < methodTypeParametersSize; ++typeParameterIndex) {
|
||||
Class<?> typeParameter = typeParameters.get(typeParameterIndex);
|
||||
|
||||
if (isValidType(typeParameter) == false) {
|
||||
throw new IllegalArgumentException("type parameter [" + typeToCanonicalTypeName(typeParameter) + "] not found " +
|
||||
"for binding [[" + targetCanonicalClassName + "], " + typesToCanonicalTypeNames(typeParameters) + "]");
|
||||
}
|
||||
|
||||
Class<?> javaTypeParameter = javaMethod.getParameterTypes()[typeParameterIndex];
|
||||
|
||||
if (isValidType(javaTypeParameter) == false) {
|
||||
throw new IllegalArgumentException("type parameter [" + typeToCanonicalTypeName(typeParameter) + "] not found " +
|
||||
"for binding [[" + targetCanonicalClassName + "], " + typesToCanonicalTypeNames(typeParameters) + "]");
|
||||
}
|
||||
|
||||
if (javaTypeParameter != typeToJavaType(typeParameter)) {
|
||||
throw new IllegalArgumentException("type parameter [" + typeToCanonicalTypeName(javaTypeParameter) + "] " +
|
||||
"does not match the specified type parameter [" + typeToCanonicalTypeName(typeParameter) + "] " +
|
||||
"for binding [[" + targetClass.getCanonicalName() + "], " + typesToCanonicalTypeNames(typeParameters) + "]");
|
||||
}
|
||||
}
|
||||
|
||||
if (javaMethod.getReturnType() != typeToJavaType(returnType)) {
|
||||
throw new IllegalArgumentException("return type [" + typeToCanonicalTypeName(javaMethod.getReturnType()) + "] " +
|
||||
"does not match the specified returned type [" + typeToCanonicalTypeName(returnType) + "] " +
|
||||
"for binding [[" + targetClass.getCanonicalName() + "], [" + methodName + "], " +
|
||||
typesToCanonicalTypeNames(typeParameters) + "]");
|
||||
}
|
||||
|
||||
String painlessMethodKey = buildPainlessMethodKey(methodName, constructorTypeParametersSize + methodTypeParametersSize);
|
||||
PainlessBinding painlessBinding = painlessMethodKeysToPainlessBindings.get(painlessMethodKey);
|
||||
|
||||
if (painlessBinding == null) {
|
||||
Constructor<?> finalJavaConstructor = javaConstructor;
|
||||
Method finalJavaMethod = javaMethod;
|
||||
|
||||
painlessBinding = painlessBindingCache.computeIfAbsent(
|
||||
new PainlessBindingCacheKey(targetClass, methodName, returnType, typeParameters),
|
||||
key -> new PainlessBinding(finalJavaConstructor, finalJavaMethod, returnType, typeParameters));
|
||||
|
||||
painlessMethodKeysToPainlessBindings.put(painlessMethodKey, painlessBinding);
|
||||
} else if (painlessBinding.javaConstructor.equals(javaConstructor) == false ||
|
||||
painlessBinding.javaMethod.equals(javaMethod) == false ||
|
||||
painlessBinding.returnType != returnType ||
|
||||
painlessBinding.typeParameters.equals(typeParameters) == false) {
|
||||
throw new IllegalArgumentException("cannot have bindings " +
|
||||
"[[" + targetCanonicalClassName + "], " +
|
||||
"[" + methodName + "], " +
|
||||
"[" + typeToCanonicalTypeName(returnType) + "], " +
|
||||
typesToCanonicalTypeNames(typeParameters) + "] and " +
|
||||
"[[" + targetCanonicalClassName + "], " +
|
||||
"[" + methodName + "], " +
|
||||
"[" + typeToCanonicalTypeName(painlessBinding.returnType) + "], " +
|
||||
typesToCanonicalTypeNames(painlessBinding.typeParameters) + "] and " +
|
||||
"with the same name and arity but different constructors or methods");
|
||||
}
|
||||
}
|
||||
|
||||
public PainlessLookup build() {
|
||||
copyPainlessClassMembers();
|
||||
cacheRuntimeHandles();
|
||||
|
@ -742,7 +976,7 @@ public final class PainlessLookupBuilder {
|
|||
classesToPainlessClasses.put(painlessClassBuilderEntry.getKey(), painlessClassBuilderEntry.getValue().build());
|
||||
}
|
||||
|
||||
return new PainlessLookup(canonicalClassNamesToClasses, classesToPainlessClasses);
|
||||
return new PainlessLookup(canonicalClassNamesToClasses, classesToPainlessClasses, painlessMethodKeysToPainlessBindings);
|
||||
}
|
||||
|
||||
private void copyPainlessClassMembers() {
|
||||
|
|
|
@ -26,6 +26,7 @@ import java.util.Collections;
|
|||
import java.util.List;
|
||||
|
||||
public class PainlessMethod {
|
||||
|
||||
public final Method javaMethod;
|
||||
public final Class<?> targetClass;
|
||||
public final Class<?> returnType;
|
||||
|
|
|
@ -24,8 +24,12 @@ import org.elasticsearch.painless.Locals;
|
|||
import org.elasticsearch.painless.Locals.LocalMethod;
|
||||
import org.elasticsearch.painless.Location;
|
||||
import org.elasticsearch.painless.MethodWriter;
|
||||
import org.elasticsearch.painless.lookup.PainlessBinding;
|
||||
import org.objectweb.asm.Label;
|
||||
import org.objectweb.asm.Type;
|
||||
import org.objectweb.asm.commons.Method;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
@ -41,6 +45,7 @@ public final class ECallLocal extends AExpression {
|
|||
private final List<AExpression> arguments;
|
||||
|
||||
private LocalMethod method = null;
|
||||
private PainlessBinding binding = null;
|
||||
|
||||
public ECallLocal(Location location, String name, List<AExpression> arguments) {
|
||||
super(location);
|
||||
|
@ -60,32 +65,71 @@ public final class ECallLocal extends AExpression {
|
|||
void analyze(Locals locals) {
|
||||
method = locals.getMethod(name, arguments.size());
|
||||
|
||||
|
||||
if (method == null) {
|
||||
throw createError(new IllegalArgumentException("Unknown call [" + name + "] with [" + arguments.size() + "] arguments."));
|
||||
binding = locals.getPainlessLookup().lookupPainlessBinding(name, arguments.size());
|
||||
|
||||
if (binding == null) {
|
||||
throw createError(new IllegalArgumentException("Unknown call [" + name + "] with [" + arguments.size() + "] arguments."));
|
||||
}
|
||||
}
|
||||
|
||||
List<Class<?>> typeParameters = new ArrayList<>(method == null ? binding.typeParameters : method.typeParameters);
|
||||
|
||||
for (int argument = 0; argument < arguments.size(); ++argument) {
|
||||
AExpression expression = arguments.get(argument);
|
||||
|
||||
expression.expected = method.typeParameters.get(argument);
|
||||
expression.expected = typeParameters.get(argument);
|
||||
expression.internal = true;
|
||||
expression.analyze(locals);
|
||||
arguments.set(argument, expression.cast(locals));
|
||||
}
|
||||
|
||||
statement = true;
|
||||
actual = method.returnType;
|
||||
actual = method == null ? binding.returnType : method.returnType;
|
||||
}
|
||||
|
||||
@Override
|
||||
void write(MethodWriter writer, Globals globals) {
|
||||
writer.writeDebugInfo(location);
|
||||
|
||||
for (AExpression argument : arguments) {
|
||||
argument.write(writer, globals);
|
||||
}
|
||||
if (method == null) {
|
||||
String name = globals.addBinding(binding.javaConstructor.getDeclaringClass());
|
||||
Type type = Type.getType(binding.javaConstructor.getDeclaringClass());
|
||||
int javaConstructorParameterCount = binding.javaConstructor.getParameterCount();
|
||||
|
||||
writer.invokeStatic(CLASS_TYPE, new Method(method.name, method.methodType.toMethodDescriptorString()));
|
||||
Label nonNull = new Label();
|
||||
|
||||
writer.loadThis();
|
||||
writer.getField(CLASS_TYPE, name, type);
|
||||
writer.ifNonNull(nonNull);
|
||||
writer.loadThis();
|
||||
writer.newInstance(type);
|
||||
writer.dup();
|
||||
|
||||
for (int argument = 0; argument < javaConstructorParameterCount; ++argument) {
|
||||
arguments.get(argument).write(writer, globals);
|
||||
}
|
||||
|
||||
writer.invokeConstructor(type, Method.getMethod(binding.javaConstructor));
|
||||
writer.putField(CLASS_TYPE, name, type);
|
||||
|
||||
writer.mark(nonNull);
|
||||
writer.loadThis();
|
||||
writer.getField(CLASS_TYPE, name, type);
|
||||
|
||||
for (int argument = 0; argument < binding.javaMethod.getParameterCount(); ++argument) {
|
||||
arguments.get(argument + javaConstructorParameterCount).write(writer, globals);
|
||||
}
|
||||
|
||||
writer.invokeVirtual(type, Method.getMethod(binding.javaMethod));
|
||||
} else {
|
||||
for (AExpression argument : arguments) {
|
||||
argument.write(writer, globals);
|
||||
}
|
||||
|
||||
writer.invokeStatic(CLASS_TYPE, new Method(method.name, method.methodType.toMethodDescriptorString()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -359,6 +359,13 @@ public final class SSource extends AStatement {
|
|||
clinit.endMethod();
|
||||
}
|
||||
|
||||
// Write binding variables
|
||||
for (Map.Entry<String, Class<?>> binding : globals.getBindings().entrySet()) {
|
||||
String name = binding.getKey();
|
||||
String descriptor = Type.getType(binding.getValue()).getDescriptor();
|
||||
visitor.visitField(Opcodes.ACC_PRIVATE, name, descriptor, null, null).visitEnd();
|
||||
}
|
||||
|
||||
// Write any needsVarName methods for used variables
|
||||
for (org.objectweb.asm.commons.Method needsMethod : scriptClassInfo.getNeedsMethods()) {
|
||||
String name = needsMethod.getName();
|
||||
|
|
|
@ -132,24 +132,6 @@ class org.elasticsearch.index.mapper.IpFieldMapper$IpFieldType$IpScriptDocValues
|
|||
List getValues()
|
||||
}
|
||||
|
||||
# for testing.
|
||||
# currently FeatureTest exposes overloaded constructor, field load store, and overloaded static methods
|
||||
class org.elasticsearch.painless.FeatureTest no_import {
|
||||
int z
|
||||
()
|
||||
(int,int)
|
||||
int getX()
|
||||
int getY()
|
||||
void setX(int)
|
||||
void setY(int)
|
||||
boolean overloadedStatic()
|
||||
boolean overloadedStatic(boolean)
|
||||
Object twoFunctionsOfX(Function,Function)
|
||||
void listInput(List)
|
||||
int org.elasticsearch.painless.FeatureTestAugmentation getTotal()
|
||||
int org.elasticsearch.painless.FeatureTestAugmentation addToTotal(int)
|
||||
}
|
||||
|
||||
class org.elasticsearch.search.lookup.FieldLookup {
|
||||
def getValue()
|
||||
List getValues()
|
||||
|
@ -174,4 +156,26 @@ class org.elasticsearch.index.similarity.ScriptedSimilarity$Term {
|
|||
class org.elasticsearch.index.similarity.ScriptedSimilarity$Doc {
|
||||
int getLength()
|
||||
float getFreq()
|
||||
}
|
||||
|
||||
# for testing
|
||||
class org.elasticsearch.painless.FeatureTest no_import {
|
||||
int z
|
||||
()
|
||||
(int,int)
|
||||
int getX()
|
||||
int getY()
|
||||
void setX(int)
|
||||
void setY(int)
|
||||
boolean overloadedStatic()
|
||||
boolean overloadedStatic(boolean)
|
||||
Object twoFunctionsOfX(Function,Function)
|
||||
void listInput(List)
|
||||
int org.elasticsearch.painless.FeatureTestAugmentation getTotal()
|
||||
int org.elasticsearch.painless.FeatureTestAugmentation addToTotal(int)
|
||||
}
|
||||
|
||||
# for testing
|
||||
static {
|
||||
int testAddWithState(int, int, int) bound_to org.elasticsearch.painless.BindingTest
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 org.elasticsearch.script.ExecutableScript;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class BindingsTests extends ScriptTestCase {
|
||||
|
||||
public void testBasicBinding() {
|
||||
assertEquals(15, exec("testAddWithState(4, 5, 6)"));
|
||||
}
|
||||
|
||||
public void testRepeatedBinding() {
|
||||
String script = "testAddWithState(4, 5, params.test)";
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
ExecutableScript.Factory factory = scriptEngine.compile(null, script, ExecutableScript.CONTEXT, Collections.emptyMap());
|
||||
ExecutableScript executableScript = factory.newInstance(params);
|
||||
|
||||
executableScript.setNextVar("test", 5);
|
||||
assertEquals(14, executableScript.run());
|
||||
|
||||
executableScript.setNextVar("test", 4);
|
||||
assertEquals(13, executableScript.run());
|
||||
|
||||
executableScript.setNextVar("test", 7);
|
||||
assertEquals(16, executableScript.run());
|
||||
}
|
||||
|
||||
public void testBoundBinding() {
|
||||
String script = "testAddWithState(4, params.bound, params.test)";
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
ExecutableScript.Factory factory = scriptEngine.compile(null, script, ExecutableScript.CONTEXT, Collections.emptyMap());
|
||||
ExecutableScript executableScript = factory.newInstance(params);
|
||||
|
||||
executableScript.setNextVar("test", 5);
|
||||
executableScript.setNextVar("bound", 1);
|
||||
assertEquals(10, executableScript.run());
|
||||
|
||||
executableScript.setNextVar("test", 4);
|
||||
executableScript.setNextVar("bound", 2);
|
||||
assertEquals(9, executableScript.run());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue