Painless: Add Bindings (#33042)

Add bindings that allow some specialized methods to store permanent state between script executions.
This commit is contained in:
Jack Conradson 2018-08-29 09:24:56 -07:00 committed by GitHub
parent 6daf8115d6
commit b52818ec6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 715 additions and 94 deletions

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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() {}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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() {

View File

@ -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;

View File

@ -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

View File

@ -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();

View File

@ -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
}

View File

@ -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());
}
}