瀏覽代碼

[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.
Jack Conradson 7 年之前
父節點
當前提交
1b085252c3
共有 16 個文件被更改,包括 533 次插入47 次删除
  1. 6 2
      modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/Whitelist.java
  2. 1 3
      modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistClassBinding.java
  3. 61 0
      modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistInstanceBinding.java
  4. 2 1
      modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistLoader.java
  5. 6 4
      modules/lang-painless/src/main/java/org/elasticsearch/painless/Compiler.java
  6. 18 7
      modules/lang-painless/src/main/java/org/elasticsearch/painless/Globals.java
  7. 0 1
      modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessClassBinding.java
  8. 64 0
      modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessInstanceBinding.java
  9. 13 1
      modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookup.java
  10. 175 12
      modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java
  11. 24 3
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECallLocal.java
  12. 22 8
      modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SSource.java
  13. 53 4
      modules/lang-painless/src/test/java/org/elasticsearch/painless/BindingsTests.java
  14. 11 1
      plugins/examples/painless-whitelist/src/main/java/org/elasticsearch/example/painlesswhitelist/ExampleWhitelistExtension.java
  15. 36 0
      plugins/examples/painless-whitelist/src/main/java/org/elasticsearch/example/painlesswhitelist/ExampleWhitelistedInstance.java
  16. 41 0
      plugins/examples/painless-whitelist/src/test/resources/rest-api-spec/test/painless_whitelist/40_instance.yml

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

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

+ 1 - 3
modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistClassBinding.java

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

+ 61 - 0
modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistInstanceBinding.java

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

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

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

+ 6 - 4
modules/lang-painless/src/main/java/org/elasticsearch/painless/Compiler.java

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

+ 18 - 7
modules/lang-painless/src/main/java/org/elasticsearch/painless/Globals.java

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

+ 0 - 1
modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessClassBinding.java

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

+ 64 - 0
modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessInstanceBinding.java

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

+ 13 - 1
modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookup.java

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

+ 175 - 12
modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java

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

+ 24 - 3
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECallLocal.java

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

+ 22 - 8
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SSource.java

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

+ 53 - 4
modules/lang-painless/src/test/java/org/elasticsearch/painless/BindingsTests.java

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

+ 11 - 1
plugins/examples/painless-whitelist/src/main/java/org/elasticsearch/example/painlesswhitelist/ExampleWhitelistExtension.java

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

+ 36 - 0
plugins/examples/painless-whitelist/src/main/java/org/elasticsearch/example/painlesswhitelist/ExampleWhitelistedInstance.java

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

+ 41 - 0
plugins/examples/painless-whitelist/src/test/resources/rest-api-spec/test/painless_whitelist/40_instance.yml

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