[Painless] Add instance bindings (#34410)

This change adds instance bindings to Painless. This binding allows a whitelisted
 method to be called on an instance instantiated prior to script compilation. 
Whitelisting must be done in code as there is no practical way to instantiate a 
useful instance from a text file (see the tests for an example). Since an 
instance can be shared by multiple scripts, each method called must be 
thread-safe.
This commit is contained in:
Jack Conradson 2018-10-24 12:57:28 -07:00 committed by GitHub
parent 5f588180f9
commit 1b085252c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 533 additions and 47 deletions

View File

@ -66,13 +66,17 @@ public final class Whitelist {
/** The {@link List} of all the whitelisted Painless class bindings. */
public final List<WhitelistClassBinding> whitelistClassBindings;
/** The {@link List} of all the whitelisted Painless instance bindings. */
public final List<WhitelistInstanceBinding> whitelistInstanceBindings;
/** Standard constructor. All values must be not {@code null}. */
public Whitelist(ClassLoader classLoader, List<WhitelistClass> whitelistClasses,
List<WhitelistMethod> whitelistImportedMethods, List<WhitelistClassBinding> whitelistClassBindings) {
public Whitelist(ClassLoader classLoader, List<WhitelistClass> whitelistClasses, List<WhitelistMethod> whitelistImportedMethods,
List<WhitelistClassBinding> whitelistClassBindings, List<WhitelistInstanceBinding> whitelistInstanceBindings) {
this.classLoader = Objects.requireNonNull(classLoader);
this.whitelistClasses = Collections.unmodifiableList(Objects.requireNonNull(whitelistClasses));
this.whitelistImportedMethods = Collections.unmodifiableList(Objects.requireNonNull(whitelistImportedMethods));
this.whitelistClassBindings = Collections.unmodifiableList(Objects.requireNonNull(whitelistClassBindings));
this.whitelistInstanceBindings = Collections.unmodifiableList(Objects.requireNonNull(whitelistInstanceBindings));
}
}

View File

@ -42,9 +42,7 @@ public class WhitelistClassBinding {
/** The method name for this class binding. */
public final String methodName;
/**
* The canonical type name for the return type.
*/
/** The canonical type name for the return type. */
public final String returnCanonicalTypeName;
/**

View File

@ -0,0 +1,61 @@
/*
* 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;
/**
* An instance binding represents a method call that stores state. Each instance binding must provide
* exactly one public method name. The canonical type name parameters provided must match those of the
* method. The method for an instance binding will target the specified Java instance.
*/
public class WhitelistInstanceBinding {
/** Information about where this constructor was whitelisted from. */
public final String origin;
/** The Java instance this instance binding targets. */
public final Object targetInstance;
/** The method name for this class 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 WhitelistInstanceBinding(String origin, Object targetInstance,
String methodName, String returnCanonicalTypeName, List<String> canonicalTypeNameParameters) {
this.origin = Objects.requireNonNull(origin);
this.targetInstance = Objects.requireNonNull(targetInstance);
this.methodName = Objects.requireNonNull(methodName);
this.returnCanonicalTypeName = Objects.requireNonNull(returnCanonicalTypeName);
this.canonicalTypeNameParameters = Objects.requireNonNull(canonicalTypeNameParameters);
}
}

View File

@ -29,6 +29,7 @@ import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/** Loads and creates a {@link Whitelist} from one to many text files. */
@ -392,7 +393,7 @@ public final class WhitelistLoader {
ClassLoader loader = AccessController.doPrivileged((PrivilegedAction<ClassLoader>)resource::getClassLoader);
return new Whitelist(loader, whitelistClasses, whitelistStatics, whitelistClassBindings);
return new Whitelist(loader, whitelistClasses, whitelistStatics, whitelistClassBindings, Collections.emptyList());
}
private WhitelistLoader() {}

View File

@ -20,7 +20,6 @@
package org.elasticsearch.painless;
import org.elasticsearch.bootstrap.BootstrapInfo;
import org.elasticsearch.painless.Locals.LocalMethod;
import org.elasticsearch.painless.antlr.Walker;
import org.elasticsearch.painless.lookup.PainlessLookup;
import org.elasticsearch.painless.node.SSource;
@ -222,8 +221,8 @@ final class Compiler {
ScriptClassInfo scriptClassInfo = new ScriptClassInfo(painlessLookup, scriptClass);
SSource root = Walker.buildPainlessTree(scriptClassInfo, reserved, name, source, settings, painlessLookup,
null);
Map<String, LocalMethod> localMethods = root.analyze(painlessLookup);
root.write();
root.analyze(painlessLookup);
Map<String, Object> statics = root.write();
try {
Class<? extends PainlessScript> clazz = loader.defineScript(CLASS_NAME, root.getBytes());
@ -231,7 +230,10 @@ final class Compiler {
clazz.getField("$SOURCE").set(null, source);
clazz.getField("$STATEMENTS").set(null, root.getStatements());
clazz.getField("$DEFINITION").set(null, painlessLookup);
clazz.getField("$LOCALS").set(null, localMethods);
for (Map.Entry<String, Object> statik : statics.entrySet()) {
clazz.getField(statik.getKey()).set(null, statik.getValue());
}
return clazz.getConstructors()[0];
} catch (Exception exception) { // Catch everything to let the user know this is something caused internally.

View File

@ -31,7 +31,8 @@ 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 Map<String,Class<?>> classBindings = new HashMap<>();
private final Map<Object,String> instanceBindings = new HashMap<>();
private final BitSet statements;
/** Create a new Globals from the set of statement boundaries */
@ -56,14 +57,19 @@ public class Globals {
}
}
/** 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);
/** Adds a new class binding to be written as a local variable */
public String addClassBinding(Class<?> type) {
String name = "$class_binding$" + classBindings.size();
classBindings.put(name, type);
return name;
}
/** Adds a new binding to be written as a local variable */
public String addInstanceBinding(Object instance) {
return instanceBindings.computeIfAbsent(instance, key -> "$instance_binding$" + instanceBindings.size());
}
/** Returns the current synthetic methods */
public Map<String,SFunction> getSyntheticMethods() {
return syntheticMethods;
@ -75,8 +81,13 @@ public class Globals {
}
/** Returns the current bindings */
public Map<String,Class<?>> getBindings() {
return bindings;
public Map<String,Class<?>> getClassBindings() {
return classBindings;
}
/** Returns the current bindings */
public Map<Object,String> getInstanceBindings() {
return instanceBindings;
}
/** Returns the set of statement boundaries */

View File

@ -60,7 +60,6 @@ public class PainlessClassBinding {
@Override
public int hashCode() {
return Objects.hash(javaConstructor, javaMethod, returnType, typeParameters);
}
}

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.lookup;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Objects;
public class PainlessInstanceBinding {
public final Object targetInstance;
public final Method javaMethod;
public final Class<?> returnType;
public final List<Class<?>> typeParameters;
PainlessInstanceBinding(Object targetInstance, Method javaMethod, Class<?> returnType, List<Class<?>> typeParameters) {
this.targetInstance = targetInstance;
this.javaMethod = javaMethod;
this.returnType = returnType;
this.typeParameters = typeParameters;
}
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (object == null || getClass() != object.getClass()) {
return false;
}
PainlessInstanceBinding that = (PainlessInstanceBinding)object;
return targetInstance == that.targetInstance &&
Objects.equals(javaMethod, that.javaMethod) &&
Objects.equals(returnType, that.returnType) &&
Objects.equals(typeParameters, that.typeParameters);
}
@Override
public int hashCode() {
return Objects.hash(targetInstance, javaMethod, returnType, typeParameters);
}
}

View File

@ -40,13 +40,15 @@ public final class PainlessLookup {
private final Map<String, PainlessMethod> painlessMethodKeysToImportedPainlessMethods;
private final Map<String, PainlessClassBinding> painlessMethodKeysToPainlessClassBindings;
private final Map<String, PainlessInstanceBinding> painlessMethodKeysToPainlessInstanceBindings;
PainlessLookup(
Map<String, Class<?>> javaClassNamesToClasses,
Map<String, Class<?>> canonicalClassNamesToClasses,
Map<Class<?>, PainlessClass> classesToPainlessClasses,
Map<String, PainlessMethod> painlessMethodKeysToImportedPainlessMethods,
Map<String, PainlessClassBinding> painlessMethodKeysToPainlessClassBindings) {
Map<String, PainlessClassBinding> painlessMethodKeysToPainlessClassBindings,
Map<String, PainlessInstanceBinding> painlessMethodKeysToPainlessInstanceBindings) {
Objects.requireNonNull(javaClassNamesToClasses);
Objects.requireNonNull(canonicalClassNamesToClasses);
@ -54,6 +56,7 @@ public final class PainlessLookup {
Objects.requireNonNull(painlessMethodKeysToImportedPainlessMethods);
Objects.requireNonNull(painlessMethodKeysToPainlessClassBindings);
Objects.requireNonNull(painlessMethodKeysToPainlessInstanceBindings);
this.javaClassNamesToClasses = javaClassNamesToClasses;
this.canonicalClassNamesToClasses = Collections.unmodifiableMap(canonicalClassNamesToClasses);
@ -61,6 +64,7 @@ public final class PainlessLookup {
this.painlessMethodKeysToImportedPainlessMethods = Collections.unmodifiableMap(painlessMethodKeysToImportedPainlessMethods);
this.painlessMethodKeysToPainlessClassBindings = Collections.unmodifiableMap(painlessMethodKeysToPainlessClassBindings);
this.painlessMethodKeysToPainlessInstanceBindings = Collections.unmodifiableMap(painlessMethodKeysToPainlessInstanceBindings);
}
public Class<?> javaClassNameToClass(String javaClassName) {
@ -200,6 +204,14 @@ public final class PainlessLookup {
return painlessMethodKeysToPainlessClassBindings.get(painlessMethodKey);
}
public PainlessInstanceBinding lookupPainlessInstanceBinding(String methodName, int arity) {
Objects.requireNonNull(methodName);
String painlessMethodKey = buildPainlessMethodKey(methodName, arity);
return painlessMethodKeysToPainlessInstanceBindings.get(painlessMethodKey);
}
public PainlessMethod lookupFunctionalInterfacePainlessMethod(Class<?> targetClass) {
PainlessClass targetPainlessClass = classesToPainlessClasses.get(targetClass);

View File

@ -24,6 +24,7 @@ import org.elasticsearch.painless.spi.WhitelistClass;
import org.elasticsearch.painless.spi.WhitelistClassBinding;
import org.elasticsearch.painless.spi.WhitelistConstructor;
import org.elasticsearch.painless.spi.WhitelistField;
import org.elasticsearch.painless.spi.WhitelistInstanceBinding;
import org.elasticsearch.painless.spi.WhitelistMethod;
import java.lang.invoke.MethodHandle;
@ -50,10 +51,11 @@ import static org.elasticsearch.painless.lookup.PainlessLookupUtility.typesToCan
public final class PainlessLookupBuilder {
private static final Map<PainlessConstructor , PainlessConstructor> painlessConstructorCache = new HashMap<>();
private static final Map<PainlessMethod , PainlessMethod> painlessMethodCache = new HashMap<>();
private static final Map<PainlessField , PainlessField> painlessFieldCache = new HashMap<>();
private static final Map<PainlessClassBinding, PainlessClassBinding> painlessClassBindingCache = new HashMap<>();
private static final Map<PainlessConstructor , PainlessConstructor> painlessConstructorCache = new HashMap<>();
private static final Map<PainlessMethod , PainlessMethod> painlessMethodCache = new HashMap<>();
private static final Map<PainlessField , PainlessField> painlessFieldCache = new HashMap<>();
private static final Map<PainlessClassBinding , PainlessClassBinding> painlessClassBindingCache = new HashMap<>();
private static final Map<PainlessInstanceBinding, PainlessInstanceBinding> painlessInstanceBindingCache = 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]*$");
@ -108,9 +110,15 @@ public final class PainlessLookupBuilder {
for (WhitelistClassBinding whitelistClassBinding : whitelist.whitelistClassBindings) {
origin = whitelistClassBinding.origin;
painlessLookupBuilder.addPainlessClassBinding(
whitelist.classLoader, whitelistClassBinding.targetJavaClassName,
whitelistClassBinding.methodName, whitelistClassBinding.returnCanonicalTypeName,
whitelistClassBinding.canonicalTypeNameParameters);
whitelist.classLoader, whitelistClassBinding.targetJavaClassName, whitelistClassBinding.methodName,
whitelistClassBinding.returnCanonicalTypeName, whitelistClassBinding.canonicalTypeNameParameters);
}
for (WhitelistInstanceBinding whitelistInstanceBinding : whitelist.whitelistInstanceBindings) {
origin = whitelistInstanceBinding.origin;
painlessLookupBuilder.addPainlessInstanceBinding(
whitelistInstanceBinding.targetInstance, whitelistInstanceBinding.methodName,
whitelistInstanceBinding.returnCanonicalTypeName, whitelistInstanceBinding.canonicalTypeNameParameters);
}
}
} catch (Exception exception) {
@ -134,6 +142,7 @@ public final class PainlessLookupBuilder {
private final Map<String, PainlessMethod> painlessMethodKeysToImportedPainlessMethods;
private final Map<String, PainlessClassBinding> painlessMethodKeysToPainlessClassBindings;
private final Map<String, PainlessInstanceBinding> painlessMethodKeysToPainlessInstanceBindings;
public PainlessLookupBuilder() {
javaClassNamesToClasses = new HashMap<>();
@ -142,6 +151,7 @@ public final class PainlessLookupBuilder {
painlessMethodKeysToImportedPainlessMethods = new HashMap<>();
painlessMethodKeysToPainlessClassBindings = new HashMap<>();
painlessMethodKeysToPainlessInstanceBindings = new HashMap<>();
}
private Class<?> canonicalTypeNameToType(String canonicalTypeName) {
@ -763,6 +773,10 @@ public final class PainlessLookupBuilder {
throw new IllegalArgumentException("imported method and class binding cannot have the same name [" + methodName + "]");
}
if (painlessMethodKeysToPainlessInstanceBindings.containsKey(painlessMethodKey)) {
throw new IllegalArgumentException("imported method and instance binding cannot have the same name [" + methodName + "]");
}
MethodHandle methodHandle;
try {
@ -783,7 +797,7 @@ public final class PainlessLookupBuilder {
painlessMethodKeysToImportedPainlessMethods.put(painlessMethodKey, newImportedPainlessMethod);
} else if (newImportedPainlessMethod.equals(existingImportedPainlessMethod) == false) {
throw new IllegalArgumentException("cannot add imported methods with the same name and arity " +
"but are not equivalent for methods " +
"but do not have equivalent methods " +
"[[" + targetCanonicalClassName + "], [" + methodName + "], " +
"[" + typeToCanonicalTypeName(returnType) + "], " +
typesToCanonicalTypeNames(typeParameters) + "] and " +
@ -942,6 +956,11 @@ public final class PainlessLookupBuilder {
}
}
if (isValidType(returnType) == false) {
throw new IllegalArgumentException("return type [" + typeToCanonicalTypeName(returnType) + "] not found for class binding " +
"[[" + targetCanonicalClassName + "], [" + methodName + "], " + typesToCanonicalTypeNames(typeParameters) + "]");
}
if (javaMethod.getReturnType() != typeToJavaType(returnType)) {
throw new IllegalArgumentException("return type [" + typeToCanonicalTypeName(javaMethod.getReturnType()) + "] " +
"does not match the specified returned type [" + typeToCanonicalTypeName(returnType) + "] " +
@ -955,6 +974,15 @@ public final class PainlessLookupBuilder {
throw new IllegalArgumentException("class binding and imported method cannot have the same name [" + methodName + "]");
}
if (painlessMethodKeysToPainlessInstanceBindings.containsKey(painlessMethodKey)) {
throw new IllegalArgumentException("class binding and instance binding cannot have the same name [" + methodName + "]");
}
if (Modifier.isStatic(javaMethod.getModifiers())) {
throw new IllegalArgumentException("class binding [[" + targetClass.getCanonicalName() + "], [" + methodName + "], " +
typesToCanonicalTypeNames(typeParameters) + "] cannot be static");
}
PainlessClassBinding existingPainlessClassBinding = painlessMethodKeysToPainlessClassBindings.get(painlessMethodKey);
PainlessClassBinding newPainlessClassBinding =
new PainlessClassBinding(javaConstructor, javaMethod, returnType, typeParameters);
@ -962,9 +990,9 @@ public final class PainlessLookupBuilder {
if (existingPainlessClassBinding == null) {
newPainlessClassBinding = painlessClassBindingCache.computeIfAbsent(newPainlessClassBinding, key -> key);
painlessMethodKeysToPainlessClassBindings.put(painlessMethodKey, newPainlessClassBinding);
} else if (newPainlessClassBinding.equals(existingPainlessClassBinding)) {
} else if (newPainlessClassBinding.equals(existingPainlessClassBinding) == false) {
throw new IllegalArgumentException("cannot add class bindings with the same name and arity " +
"but are not equivalent for methods " +
"but do not have equivalent methods " +
"[[" + targetCanonicalClassName + "], " +
"[" + methodName + "], " +
"[" + typeToCanonicalTypeName(returnType) + "], " +
@ -976,6 +1004,136 @@ public final class PainlessLookupBuilder {
}
}
public void addPainlessInstanceBinding(Object targetInstance,
String methodName, String returnCanonicalTypeName, List<String> canonicalTypeNameParameters) {
Objects.requireNonNull(targetInstance);
Objects.requireNonNull(methodName);
Objects.requireNonNull(returnCanonicalTypeName);
Objects.requireNonNull(canonicalTypeNameParameters);
Class<?> targetClass = targetInstance.getClass();
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 instance binding " +
"[[" + targetCanonicalClassName + "], [" + methodName + "], " + canonicalTypeNameParameters + "]");
}
typeParameters.add(typeParameter);
}
Class<?> returnType = canonicalTypeNameToType(returnCanonicalTypeName);
if (returnType == null) {
throw new IllegalArgumentException("return type [" + returnCanonicalTypeName + "] not found for class binding " +
"[[" + targetCanonicalClassName + "], [" + methodName + "], " + canonicalTypeNameParameters + "]");
}
addPainlessInstanceBinding(targetInstance, methodName, returnType, typeParameters);
}
public void addPainlessInstanceBinding(Object targetInstance, String methodName, Class<?> returnType, List<Class<?>> typeParameters) {
Objects.requireNonNull(targetInstance);
Objects.requireNonNull(methodName);
Objects.requireNonNull(returnType);
Objects.requireNonNull(typeParameters);
Class<?> targetClass = targetInstance.getClass();
if (targetClass == def.class) {
throw new IllegalArgumentException("cannot add instance binding as reserved class [" + DEF_CLASS_NAME + "]");
}
String targetCanonicalClassName = typeToCanonicalTypeName(targetClass);
Class<?> existingTargetClass = javaClassNamesToClasses.get(targetClass.getName());
if (existingTargetClass == null) {
javaClassNamesToClasses.put(targetClass.getName(), targetClass);
} else if (existingTargetClass != targetClass) {
throw new IllegalArgumentException("class [" + targetCanonicalClassName + "] " +
"cannot represent multiple java classes with the same name from different class loaders");
}
if (METHOD_NAME_PATTERN.matcher(methodName).matches() == false) {
throw new IllegalArgumentException(
"invalid method name [" + methodName + "] for instance binding [" + targetCanonicalClassName + "].");
}
int typeParametersSize = typeParameters.size();
List<Class<?>> javaTypeParameters = new ArrayList<>(typeParametersSize);
for (Class<?> typeParameter : typeParameters) {
if (isValidType(typeParameter) == false) {
throw new IllegalArgumentException("type parameter [" + typeToCanonicalTypeName(typeParameter) + "] " +
"not found for instance binding [[" + targetCanonicalClassName + "], [" + methodName + "], " +
typesToCanonicalTypeNames(typeParameters) + "]");
}
javaTypeParameters.add(typeToJavaType(typeParameter));
}
if (isValidType(returnType) == false) {
throw new IllegalArgumentException("return type [" + typeToCanonicalTypeName(returnType) + "] not found for imported method " +
"[[" + targetCanonicalClassName + "], [" + methodName + "], " + typesToCanonicalTypeNames(typeParameters) + "]");
}
Method javaMethod;
try {
javaMethod = targetClass.getMethod(methodName, javaTypeParameters.toArray(new Class<?>[typeParametersSize]));
} catch (NoSuchMethodException nsme) {
throw new IllegalArgumentException("instance binding reflection object [[" + targetCanonicalClassName + "], " +
"[" + methodName + "], " + typesToCanonicalTypeNames(typeParameters) + "] not found", nsme);
}
if (javaMethod.getReturnType() != typeToJavaType(returnType)) {
throw new IllegalArgumentException("return type [" + typeToCanonicalTypeName(javaMethod.getReturnType()) + "] " +
"does not match the specified returned type [" + typeToCanonicalTypeName(returnType) + "] " +
"for instance binding [[" + targetClass.getCanonicalName() + "], [" + methodName + "], " +
typesToCanonicalTypeNames(typeParameters) + "]");
}
if (Modifier.isStatic(javaMethod.getModifiers())) {
throw new IllegalArgumentException("instance binding [[" + targetClass.getCanonicalName() + "], [" + methodName + "], " +
typesToCanonicalTypeNames(typeParameters) + "] cannot be static");
}
String painlessMethodKey = buildPainlessMethodKey(methodName, typeParametersSize);
if (painlessMethodKeysToImportedPainlessMethods.containsKey(painlessMethodKey)) {
throw new IllegalArgumentException("instance binding and imported method cannot have the same name [" + methodName + "]");
}
if (painlessMethodKeysToPainlessClassBindings.containsKey(painlessMethodKey)) {
throw new IllegalArgumentException("instance binding and class binding cannot have the same name [" + methodName + "]");
}
PainlessInstanceBinding existingPainlessInstanceBinding = painlessMethodKeysToPainlessInstanceBindings.get(painlessMethodKey);
PainlessInstanceBinding newPainlessInstanceBinding =
new PainlessInstanceBinding(targetInstance, javaMethod, returnType, typeParameters);
if (existingPainlessInstanceBinding == null) {
newPainlessInstanceBinding = painlessInstanceBindingCache.computeIfAbsent(newPainlessInstanceBinding, key -> key);
painlessMethodKeysToPainlessInstanceBindings.put(painlessMethodKey, newPainlessInstanceBinding);
} else if (newPainlessInstanceBinding.equals(existingPainlessInstanceBinding) == false) {
throw new IllegalArgumentException("cannot add instances bindings with the same name and arity " +
"but do not have equivalent methods " +
"[[" + targetCanonicalClassName + "], " +
"[" + methodName + "], " +
"[" + typeToCanonicalTypeName(returnType) + "], " +
typesToCanonicalTypeNames(typeParameters) + "] and " +
"[[" + targetCanonicalClassName + "], " +
"[" + methodName + "], " +
"[" + typeToCanonicalTypeName(existingPainlessInstanceBinding.returnType) + "], " +
typesToCanonicalTypeNames(existingPainlessInstanceBinding.typeParameters) + "]");
}
}
public PainlessLookup build() {
copyPainlessClassMembers();
cacheRuntimeHandles();
@ -1003,8 +1161,13 @@ public final class PainlessLookupBuilder {
"must have the same classes as the keys of classes to painless classes");
}
return new PainlessLookup(javaClassNamesToClasses, canonicalClassNamesToClasses, classesToPainlessClasses,
painlessMethodKeysToImportedPainlessMethods, painlessMethodKeysToPainlessClassBindings);
return new PainlessLookup(
javaClassNamesToClasses,
canonicalClassNamesToClasses,
classesToPainlessClasses,
painlessMethodKeysToImportedPainlessMethods,
painlessMethodKeysToPainlessClassBindings,
painlessMethodKeysToPainlessInstanceBindings);
}
private void copyPainlessClassMembers() {

View File

@ -25,6 +25,7 @@ import org.elasticsearch.painless.Locals.LocalMethod;
import org.elasticsearch.painless.Location;
import org.elasticsearch.painless.MethodWriter;
import org.elasticsearch.painless.lookup.PainlessClassBinding;
import org.elasticsearch.painless.lookup.PainlessInstanceBinding;
import org.elasticsearch.painless.lookup.PainlessMethod;
import org.objectweb.asm.Label;
import org.objectweb.asm.Type;
@ -48,6 +49,7 @@ public final class ECallLocal extends AExpression {
private LocalMethod localMethod = null;
private PainlessMethod importedMethod = null;
private PainlessClassBinding classBinding = null;
private PainlessInstanceBinding instanceBinding = null;
public ECallLocal(Location location, String name, List<AExpression> arguments) {
super(location);
@ -74,8 +76,12 @@ public final class ECallLocal extends AExpression {
classBinding = locals.getPainlessLookup().lookupPainlessClassBinding(name, arguments.size());
if (classBinding == null) {
throw createError(
new IllegalArgumentException("Unknown call [" + name + "] with [" + arguments.size() + "] arguments."));
instanceBinding = locals.getPainlessLookup().lookupPainlessInstanceBinding(name, arguments.size());
if (instanceBinding == null) {
throw createError(
new IllegalArgumentException("Unknown call [" + name + "] with [" + arguments.size() + "] arguments."));
}
}
}
}
@ -91,6 +97,9 @@ public final class ECallLocal extends AExpression {
} else if (classBinding != null) {
typeParameters = new ArrayList<>(classBinding.typeParameters);
actual = classBinding.returnType;
} else if (instanceBinding != null) {
typeParameters = new ArrayList<>(instanceBinding.typeParameters);
actual = instanceBinding.returnType;
} else {
throw new IllegalStateException("Illegal tree structure.");
}
@ -125,7 +134,7 @@ public final class ECallLocal extends AExpression {
writer.invokeStatic(Type.getType(importedMethod.targetClass),
new Method(importedMethod.javaMethod.getName(), importedMethod.methodType.toMethodDescriptorString()));
} else if (classBinding != null) {
String name = globals.addBinding(classBinding.javaConstructor.getDeclaringClass());
String name = globals.addClassBinding(classBinding.javaConstructor.getDeclaringClass());
Type type = Type.getType(classBinding.javaConstructor.getDeclaringClass());
int javaConstructorParameterCount = classBinding.javaConstructor.getParameterCount();
@ -154,6 +163,18 @@ public final class ECallLocal extends AExpression {
}
writer.invokeVirtual(type, Method.getMethod(classBinding.javaMethod));
} else if (instanceBinding != null) {
String name = globals.addInstanceBinding(instanceBinding.targetInstance);
Type type = Type.getType(instanceBinding.targetInstance.getClass());
writer.loadThis();
writer.getStatic(CLASS_TYPE, name, type);
for (int argument = 0; argument < instanceBinding.javaMethod.getParameterCount(); ++argument) {
arguments.get(argument).write(writer, globals);
}
writer.invokeVirtual(type, Method.getMethod(instanceBinding.javaMethod));
} else {
throw new IllegalStateException("Illegal tree structure.");
}

View File

@ -164,7 +164,7 @@ public final class SSource extends AStatement {
throw new IllegalStateException("Illegal tree structure.");
}
public Map<String, LocalMethod> analyze(PainlessLookup painlessLookup) {
public void analyze(PainlessLookup painlessLookup) {
Map<String, LocalMethod> methods = new HashMap<>();
for (SFunction function : functions) {
@ -180,8 +180,6 @@ public final class SSource extends AStatement {
Locals locals = Locals.newProgramScope(painlessLookup, methods.values());
analyze(locals);
return locals.getMethods();
}
@Override
@ -228,7 +226,7 @@ public final class SSource extends AStatement {
}
}
public void write() {
public Map<String, Object> write() {
// Create the ClassWriter.
int classFrames = ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS;
@ -359,13 +357,20 @@ 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();
// Write class binding variables
for (Map.Entry<String, Class<?>> classBinding : globals.getClassBindings().entrySet()) {
String name = classBinding.getKey();
String descriptor = Type.getType(classBinding.getValue()).getDescriptor();
visitor.visitField(Opcodes.ACC_PRIVATE, name, descriptor, null, null).visitEnd();
}
// Write instance binding variables
for (Map.Entry<Object, String> instanceBinding : globals.getInstanceBindings().entrySet()) {
String name = instanceBinding.getValue();
String descriptor = Type.getType(instanceBinding.getKey().getClass()).getDescriptor();
visitor.visitField(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, 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();
@ -382,6 +387,15 @@ public final class SSource extends AStatement {
visitor.visitEnd();
bytes = writer.toByteArray();
Map<String, Object> statics = new HashMap<>();
statics.put("$LOCALS", mainMethod.getMethods());
for (Map.Entry<Object, String> instanceBinding : globals.getInstanceBindings().entrySet()) {
statics.put(instanceBinding.getValue(), instanceBinding.getKey());
}
return statics;
}
@Override

View File

@ -20,14 +20,32 @@
package org.elasticsearch.painless;
import org.elasticsearch.painless.spi.Whitelist;
import org.elasticsearch.painless.spi.WhitelistInstanceBinding;
import org.elasticsearch.script.ScriptContext;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class BindingsTests extends ScriptTestCase {
public static class InstanceBindingTestClass {
private int value;
public InstanceBindingTestClass(int value) {
this.value = value;
}
public void setInstanceBindingValue(int value) {
this.value = value;
}
public int getInstanceBindingValue() {
return value;
}
}
public abstract static class BindingsTestScript {
public static final String[] PARAMETERS = { "test", "bound" };
public abstract int execute(int test, int bound);
@ -40,15 +58,29 @@ public class BindingsTests extends ScriptTestCase {
@Override
protected Map<ScriptContext<?>, List<Whitelist>> scriptContexts() {
Map<ScriptContext<?>, List<Whitelist>> contexts = super.scriptContexts();
contexts.put(BindingsTestScript.CONTEXT, Whitelist.BASE_WHITELISTS);
List<Whitelist> whitelists = new ArrayList<>(Whitelist.BASE_WHITELISTS);
InstanceBindingTestClass instanceBindingTestClass = new InstanceBindingTestClass(1);
WhitelistInstanceBinding getter = new WhitelistInstanceBinding("test", instanceBindingTestClass,
"setInstanceBindingValue", "void", Collections.singletonList("int"));
WhitelistInstanceBinding setter = new WhitelistInstanceBinding("test", instanceBindingTestClass,
"getInstanceBindingValue", "int", Collections.emptyList());
List<WhitelistInstanceBinding> instanceBindingsList = new ArrayList<>();
instanceBindingsList.add(getter);
instanceBindingsList.add(setter);
Whitelist instanceBindingsWhitelist = new Whitelist(instanceBindingTestClass.getClass().getClassLoader(),
Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), instanceBindingsList);
whitelists.add(instanceBindingsWhitelist);
contexts.put(BindingsTestScript.CONTEXT, whitelists);
return contexts;
}
public void testBasicBinding() {
public void testBasicClassBinding() {
assertEquals(15, exec("testAddWithState(4, 5, 6, 0.0)"));
}
public void testRepeatedBinding() {
public void testRepeatedClassBinding() {
String script = "testAddWithState(4, 5, test, 0.0)";
BindingsTestScript.Factory factory = scriptEngine.compile(null, script, BindingsTestScript.CONTEXT, Collections.emptyMap());
BindingsTestScript executableScript = factory.newInstance();
@ -58,7 +90,7 @@ public class BindingsTests extends ScriptTestCase {
assertEquals(16, executableScript.execute(7, 0));
}
public void testBoundBinding() {
public void testBoundClassBinding() {
String script = "testAddWithState(4, bound, test, 0.0)";
BindingsTestScript.Factory factory = scriptEngine.compile(null, script, BindingsTestScript.CONTEXT, Collections.emptyMap());
BindingsTestScript executableScript = factory.newInstance();
@ -66,4 +98,21 @@ public class BindingsTests extends ScriptTestCase {
assertEquals(10, executableScript.execute(5, 1));
assertEquals(9, executableScript.execute(4, 2));
}
public void testInstanceBinding() {
String script = "getInstanceBindingValue() + test + bound";
BindingsTestScript.Factory factory = scriptEngine.compile(null, script, BindingsTestScript.CONTEXT, Collections.emptyMap());
BindingsTestScript executableScript = factory.newInstance();
assertEquals(3, executableScript.execute(1, 1));
script = "setInstanceBindingValue(test + bound); getInstanceBindingValue()";
factory = scriptEngine.compile(null, script, BindingsTestScript.CONTEXT, Collections.emptyMap());
executableScript = factory.newInstance();
assertEquals(4, executableScript.execute(-2, 6));
script = "getInstanceBindingValue() + test + bound";
factory = scriptEngine.compile(null, script, BindingsTestScript.CONTEXT, Collections.emptyMap());
executableScript = factory.newInstance();
assertEquals(8, executableScript.execute(-2, 6));
}
}

View File

@ -21,10 +21,12 @@ package org.elasticsearch.example.painlesswhitelist;
import org.elasticsearch.painless.spi.PainlessExtension;
import org.elasticsearch.painless.spi.Whitelist;
import org.elasticsearch.painless.spi.WhitelistInstanceBinding;
import org.elasticsearch.painless.spi.WhitelistLoader;
import org.elasticsearch.script.FieldScript;
import org.elasticsearch.script.ScriptContext;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@ -37,6 +39,14 @@ public class ExampleWhitelistExtension implements PainlessExtension {
@Override
public Map<ScriptContext<?>, List<Whitelist>> getContextWhitelists() {
return Collections.singletonMap(FieldScript.CONTEXT, Collections.singletonList(WHITELIST));
ExampleWhitelistedInstance ewi = new ExampleWhitelistedInstance(1);
WhitelistInstanceBinding addValue = new WhitelistInstanceBinding("example addValue", ewi,
"addValue", "int", Collections.singletonList("int"));
WhitelistInstanceBinding getValue = new WhitelistInstanceBinding("example getValue", ewi,
"getValue", "int", Collections.emptyList());
Whitelist instanceWhitelist = new Whitelist(ewi.getClass().getClassLoader(), Collections.emptyList(),
Collections.emptyList(), Collections.emptyList(), Arrays.asList(addValue, getValue));
return Collections.singletonMap(FieldScript.CONTEXT, Arrays.asList(WHITELIST, instanceWhitelist));
}
}

View File

@ -0,0 +1,36 @@
/*
* 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.example.painlesswhitelist;
public class ExampleWhitelistedInstance {
private final int value;
public ExampleWhitelistedInstance(int value) {
this.value = value;
}
public int addValue(int value) {
return this.value + value;
}
public int getValue() {
return value;
}
}

View File

@ -0,0 +1,41 @@
# Example tests using an instance binding
"custom instance binding":
- do:
index:
index: test
type: test
id: 1
body: { "num1": 1 }
- do:
indices.refresh: {}
- do:
index: test
search:
body:
query:
match_all: {}
script_fields:
sNum1:
script:
source: "addValue((int)doc['num1'][0])"
lang: painless
- match: { hits.total: 1 }
- match: { hits.hits.0.fields.sNum1.0: 2 }
- do:
index: test
search:
body:
query:
match_all: {}
script_fields:
sNum1:
script:
source: "getValue() + doc['num1'][0]"
lang: painless
- match: { hits.total: 1 }
- match: { hits.hits.0.fields.sNum1.0: 2 }