diff --git a/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/Whitelist.java b/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/Whitelist.java index c36625ad145be..b08b38d2bfc7b 100644 --- a/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/Whitelist.java +++ b/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 whitelistClassBindings; + /** The {@link List} of all the whitelisted Painless instance bindings. */ + public final List whitelistInstanceBindings; + /** Standard constructor. All values must be not {@code null}. */ - public Whitelist(ClassLoader classLoader, List whitelistClasses, - List whitelistImportedMethods, List whitelistClassBindings) { + public Whitelist(ClassLoader classLoader, List whitelistClasses, List whitelistImportedMethods, + List whitelistClassBindings, List 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)); } } diff --git a/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistClassBinding.java b/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistClassBinding.java index f1e762b37c02f..da19917464820 100644 --- a/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistClassBinding.java +++ b/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; /** diff --git a/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistInstanceBinding.java b/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistInstanceBinding.java new file mode 100644 index 0000000000000..46c2f0f91fe02 --- /dev/null +++ b/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 canonicalTypeNameParameters; + + /** Standard constructor. All values must be not {@code null}. */ + public WhitelistInstanceBinding(String origin, Object targetInstance, + String methodName, String returnCanonicalTypeName, List 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); + } +} diff --git a/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistLoader.java b/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistLoader.java index 560010a35e9be..d896c345a47e9 100644 --- a/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistLoader.java +++ b/modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/WhitelistLoader.java @@ -29,6 +29,7 @@ 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 static Whitelist loadFromResourceFiles(Class resource, String... filep ClassLoader loader = AccessController.doPrivileged((PrivilegedAction)resource::getClassLoader); - return new Whitelist(loader, whitelistClasses, whitelistStatics, whitelistClassBindings); + return new Whitelist(loader, whitelistClasses, whitelistStatics, whitelistClassBindings, Collections.emptyList()); } private WhitelistLoader() {} diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/Compiler.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/Compiler.java index 0fbdfa763eae3..81cc802916d4e 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/Compiler.java +++ b/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 @@ Constructor compile(Loader loader, MainMethodReserved reserved, String name, ScriptClassInfo scriptClassInfo = new ScriptClassInfo(painlessLookup, scriptClass); SSource root = Walker.buildPainlessTree(scriptClassInfo, reserved, name, source, settings, painlessLookup, null); - Map localMethods = root.analyze(painlessLookup); - root.write(); + root.analyze(painlessLookup); + Map statics = root.write(); try { Class clazz = loader.defineScript(CLASS_NAME, root.getBytes()); @@ -231,7 +230,10 @@ Constructor compile(Loader loader, MainMethodReserved reserved, String name, 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 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. diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/Globals.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/Globals.java index d18cf2780cf3c..a6a15b8ce1e67 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/Globals.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/Globals.java @@ -31,7 +31,8 @@ public class Globals { private final Map syntheticMethods = new HashMap<>(); private final Map constantInitializers = new HashMap<>(); - private final Map> bindings = new HashMap<>(); + private final Map> classBindings = new HashMap<>(); + private final Map instanceBindings = new HashMap<>(); private final BitSet statements; /** Create a new Globals from the set of statement boundaries */ @@ -56,14 +57,19 @@ public void addConstantInitializer(Constant constant) { } } - /** 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 getSyntheticMethods() { return syntheticMethods; @@ -75,8 +81,13 @@ public Map getConstantInitializers() { } /** Returns the current bindings */ - public Map> getBindings() { - return bindings; + public Map> getClassBindings() { + return classBindings; + } + + /** Returns the current bindings */ + public Map getInstanceBindings() { + return instanceBindings; } /** Returns the set of statement boundaries */ diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessClassBinding.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessClassBinding.java index 0f28830b3d4ab..aedbc936bb1d4 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessClassBinding.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessClassBinding.java @@ -60,7 +60,6 @@ public boolean equals(Object object) { @Override public int hashCode() { - return Objects.hash(javaConstructor, javaMethod, returnType, typeParameters); } } diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessInstanceBinding.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessInstanceBinding.java new file mode 100644 index 0000000000000..6952a3f05fb64 --- /dev/null +++ b/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> typeParameters; + + PainlessInstanceBinding(Object targetInstance, Method javaMethod, Class returnType, List> 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); + } +} diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookup.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookup.java index ce31db43eeff3..5ac5d5bf7847d 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookup.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookup.java @@ -40,13 +40,15 @@ public final class PainlessLookup { private final Map painlessMethodKeysToImportedPainlessMethods; private final Map painlessMethodKeysToPainlessClassBindings; + private final Map painlessMethodKeysToPainlessInstanceBindings; PainlessLookup( Map> javaClassNamesToClasses, Map> canonicalClassNamesToClasses, Map, PainlessClass> classesToPainlessClasses, Map painlessMethodKeysToImportedPainlessMethods, - Map painlessMethodKeysToPainlessClassBindings) { + Map painlessMethodKeysToPainlessClassBindings, + Map 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 PainlessClassBinding lookupPainlessClassBinding(String methodName, int ar 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); diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java index 552ad56f68a63..495a4ea94c962 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupBuilder.java @@ -24,6 +24,7 @@ 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 @@ public final class PainlessLookupBuilder { - private static final Map painlessConstructorCache = new HashMap<>(); - private static final Map painlessMethodCache = new HashMap<>(); - private static final Map painlessFieldCache = new HashMap<>(); - private static final Map painlessClassBindingCache = new HashMap<>(); + private static final Map painlessConstructorCache = new HashMap<>(); + private static final Map painlessMethodCache = new HashMap<>(); + private static final Map painlessFieldCache = new HashMap<>(); + private static final Map painlessClassBindingCache = new HashMap<>(); + private static final Map 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 static PainlessLookup buildFromWhitelists(List whitelists) { 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 static PainlessLookup buildFromWhitelists(List whitelists) { private final Map painlessMethodKeysToImportedPainlessMethods; private final Map painlessMethodKeysToPainlessClassBindings; + private final Map painlessMethodKeysToPainlessInstanceBindings; public PainlessLookupBuilder() { javaClassNamesToClasses = new HashMap<>(); @@ -142,6 +151,7 @@ public PainlessLookupBuilder() { painlessMethodKeysToImportedPainlessMethods = new HashMap<>(); painlessMethodKeysToPainlessClassBindings = new HashMap<>(); + painlessMethodKeysToPainlessInstanceBindings = new HashMap<>(); } private Class canonicalTypeNameToType(String canonicalTypeName) { @@ -763,6 +773,10 @@ public void addImportedPainlessMethod(Class targetClass, String methodName, C 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 void addImportedPainlessMethod(Class targetClass, String methodName, C 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 void addPainlessClassBinding(Class targetClass, String methodName, Cla } } + 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 void addPainlessClassBinding(Class targetClass, String methodName, Cla 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 void addPainlessClassBinding(Class targetClass, String methodName, Cla 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 void addPainlessClassBinding(Class targetClass, String methodName, Cla } } + public void addPainlessInstanceBinding(Object targetInstance, + String methodName, String returnCanonicalTypeName, List canonicalTypeNameParameters) { + + Objects.requireNonNull(targetInstance); + Objects.requireNonNull(methodName); + Objects.requireNonNull(returnCanonicalTypeName); + Objects.requireNonNull(canonicalTypeNameParameters); + + Class targetClass = targetInstance.getClass(); + String targetCanonicalClassName = typeToCanonicalTypeName(targetClass); + List> 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> 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> 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 PainlessLookup build() { "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() { diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECallLocal.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECallLocal.java index e613018dbc54c..2d49f4df6483c 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECallLocal.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ECallLocal.java @@ -25,6 +25,7 @@ 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 arguments) { super(location); @@ -74,8 +76,12 @@ void analyze(Locals locals) { 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 @@ void analyze(Locals locals) { } 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 @@ void write(MethodWriter writer, Globals globals) { 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 @@ void write(MethodWriter writer, Globals globals) { } 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."); } diff --git a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SSource.java b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SSource.java index 01946066af99a..ca20cf19a5f0b 100644 --- a/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SSource.java +++ b/modules/lang-painless/src/main/java/org/elasticsearch/painless/node/SSource.java @@ -164,7 +164,7 @@ void extractVariables(Set variables) { throw new IllegalStateException("Illegal tree structure."); } - public Map analyze(PainlessLookup painlessLookup) { + public void analyze(PainlessLookup painlessLookup) { Map methods = new HashMap<>(); for (SFunction function : functions) { @@ -180,8 +180,6 @@ public Map analyze(PainlessLookup painlessLookup) { Locals locals = Locals.newProgramScope(painlessLookup, methods.values()); analyze(locals); - - return locals.getMethods(); } @Override @@ -228,7 +226,7 @@ void analyze(Locals program) { } } - public void write() { + public Map write() { // Create the ClassWriter. int classFrames = ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS; @@ -359,13 +357,20 @@ public void write() { clinit.endMethod(); } - // Write binding variables - for (Map.Entry> binding : globals.getBindings().entrySet()) { - String name = binding.getKey(); - String descriptor = Type.getType(binding.getValue()).getDescriptor(); + // Write class binding variables + for (Map.Entry> 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 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 void write() { visitor.visitEnd(); bytes = writer.toByteArray(); + + Map statics = new HashMap<>(); + statics.put("$LOCALS", mainMethod.getMethods()); + + for (Map.Entry instanceBinding : globals.getInstanceBindings().entrySet()) { + statics.put(instanceBinding.getValue(), instanceBinding.getKey()); + } + + return statics; } @Override diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/BindingsTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/BindingsTests.java index 167deb3a20bf9..3f3d589702a5e 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/BindingsTests.java +++ b/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 interface Factory { @Override protected Map, List> scriptContexts() { Map, List> contexts = super.scriptContexts(); - contexts.put(BindingsTestScript.CONTEXT, Whitelist.BASE_WHITELISTS); + List 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 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 void testRepeatedBinding() { 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 void testBoundBinding() { 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)); + } } diff --git a/plugins/examples/painless-whitelist/src/main/java/org/elasticsearch/example/painlesswhitelist/ExampleWhitelistExtension.java b/plugins/examples/painless-whitelist/src/main/java/org/elasticsearch/example/painlesswhitelist/ExampleWhitelistExtension.java index ca35db5a81b3d..d0b03708a08b3 100644 --- a/plugins/examples/painless-whitelist/src/main/java/org/elasticsearch/example/painlesswhitelist/ExampleWhitelistExtension.java +++ b/plugins/examples/painless-whitelist/src/main/java/org/elasticsearch/example/painlesswhitelist/ExampleWhitelistExtension.java @@ -21,10 +21,12 @@ 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, List> 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)); } } diff --git a/plugins/examples/painless-whitelist/src/main/java/org/elasticsearch/example/painlesswhitelist/ExampleWhitelistedInstance.java b/plugins/examples/painless-whitelist/src/main/java/org/elasticsearch/example/painlesswhitelist/ExampleWhitelistedInstance.java new file mode 100644 index 0000000000000..1d48cecb9b45d --- /dev/null +++ b/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; + } +} diff --git a/plugins/examples/painless-whitelist/src/test/resources/rest-api-spec/test/painless_whitelist/40_instance.yml b/plugins/examples/painless-whitelist/src/test/resources/rest-api-spec/test/painless_whitelist/40_instance.yml new file mode 100644 index 0000000000000..6cb7e4f3d401d --- /dev/null +++ b/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 }