Skip to content

Commit

Permalink
Painless: Add Bindings (#33042)
Browse files Browse the repository at this point in the history
Add bindings that allow some specialized methods to store permanent state between script executions.
  • Loading branch information
jdconrad authored Aug 29, 2018
1 parent 6daf811 commit b52818e
Show file tree
Hide file tree
Showing 19 changed files with 712 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,12 @@ public final class Whitelist {
/** The {@link List} of all the whitelisted Painless classes. */
public final List<WhitelistClass> whitelistClasses;

public final List<WhitelistBinding> whitelistBindings;

/** Standard constructor. All values must be not {@code null}. */
public Whitelist(ClassLoader classLoader, List<WhitelistClass> whitelistClasses) {
public Whitelist(ClassLoader classLoader, List<WhitelistClass> whitelistClasses, List<WhitelistBinding> whitelistBindings) {
this.classLoader = Objects.requireNonNull(classLoader);
this.whitelistClasses = Collections.unmodifiableList(Objects.requireNonNull(whitelistClasses));
this.whitelistBindings = Collections.unmodifiableList(Objects.requireNonNull(whitelistBindings));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.elasticsearch.painless.spi;

import java.util.List;
import java.util.Objects;

/**
* A binding represents a method call that stores state. Each binding class must have exactly one
* public constructor and one public method excluding those inherited directly from {@link Object}.
* The canonical type name parameters provided must match those of the constructor and method combined.
* The constructor for a binding class will be called when the binding method is called for the first
* time at which point state may be stored for the arguments passed into the constructor. The method
* for a binding class will be called each time the binding method is called and may use the previously
* stored state.
*/
public class WhitelistBinding {

/** Information about where this constructor was whitelisted from. */
public final String origin;

/** The Java class name this binding represents. */
public final String targetJavaClassName;

/** The method name for this binding. */
public final String methodName;

/**
* The canonical type name for the return type.
*/
public final String returnCanonicalTypeName;

/**
* A {@link List} of {@link String}s that are the Painless type names for the parameters of the
* constructor which can be used to look up the Java constructor through reflection.
*/
public final List<String> canonicalTypeNameParameters;

/** Standard constructor. All values must be not {@code null}. */
public WhitelistBinding(String origin, String targetJavaClassName,
String methodName, String returnCanonicalTypeName, List<String> canonicalTypeNameParameters) {

this.origin = Objects.requireNonNull(origin);
this.targetJavaClassName = Objects.requireNonNull(targetJavaClassName);

this.methodName = Objects.requireNonNull(methodName);
this.returnCanonicalTypeName = Objects.requireNonNull(returnCanonicalTypeName);
this.canonicalTypeNameParameters = Objects.requireNonNull(canonicalTypeNameParameters);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,8 @@ public final class WhitelistClass {

/** Standard constructor. All values must be not {@code null}. */
public WhitelistClass(String origin, String javaClassName, boolean noImport,
List<WhitelistConstructor> whitelistConstructors,
List<WhitelistMethod> whitelistMethods,
List<WhitelistField> whitelistFields) {
List<WhitelistConstructor> whitelistConstructors, List<WhitelistMethod> whitelistMethods, List<WhitelistField> whitelistFields)
{

this.origin = Objects.requireNonNull(origin);
this.javaClassName = Objects.requireNonNull(javaClassName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ public final class WhitelistLoader {
*/
public static Whitelist loadFromResourceFiles(Class<?> resource, String... filepaths) {
List<WhitelistClass> whitelistClasses = new ArrayList<>();
List<WhitelistBinding> whitelistBindings = new ArrayList<>();

// Execute a single pass through the whitelist text files. This will gather all the
// constructors, methods, augmented methods, and fields for each whitelisted class.
Expand All @@ -141,8 +142,9 @@ public static Whitelist loadFromResourceFiles(Class<?> resource, String... filep
int number = -1;

try (LineNumberReader reader = new LineNumberReader(
new InputStreamReader(resource.getResourceAsStream(filepath), StandardCharsets.UTF_8))) {
new InputStreamReader(resource.getResourceAsStream(filepath), StandardCharsets.UTF_8))) {

String parseType = null;
String whitelistClassOrigin = null;
String javaClassName = null;
boolean noImport = false;
Expand All @@ -165,7 +167,11 @@ public static Whitelist loadFromResourceFiles(Class<?> resource, String... filep
// Ensure the final token of the line is '{'.
if (line.endsWith("{") == false) {
throw new IllegalArgumentException(
"invalid class definition: failed to parse class opening bracket [" + line + "]");
"invalid class definition: failed to parse class opening bracket [" + line + "]");
}

if (parseType != null) {
throw new IllegalArgumentException("invalid definition: cannot embed class definition [" + line + "]");
}

// Parse the Java class name.
Expand All @@ -178,41 +184,125 @@ public static Whitelist loadFromResourceFiles(Class<?> resource, String... filep
throw new IllegalArgumentException("invalid class definition: failed to parse class name [" + line + "]");
}

parseType = "class";
whitelistClassOrigin = "[" + filepath + "]:[" + number + "]";
javaClassName = tokens[0];

// Reset all the constructors, methods, and fields to support a new class.
whitelistConstructors = new ArrayList<>();
whitelistMethods = new ArrayList<>();
whitelistFields = new ArrayList<>();
} else if (line.startsWith("static ")) {
// Ensure the final token of the line is '{'.
if (line.endsWith("{") == false) {
throw new IllegalArgumentException(
"invalid static definition: failed to parse static opening bracket [" + line + "]");
}

// Handle the end of a class, by creating a new WhitelistClass with all the previously gathered
// constructors, methods, augmented methods, and fields, and adding it to the list of whitelisted classes.
if (parseType != null) {
throw new IllegalArgumentException("invalid definition: cannot embed static definition [" + line + "]");
}

parseType = "static";

// Handle the end of a definition and reset all previously gathered values.
// Expects the following format: '}' '\n'
} else if (line.equals("}")) {
if (javaClassName == null) {
throw new IllegalArgumentException("invalid class definition: extraneous closing bracket");
if (parseType == null) {
throw new IllegalArgumentException("invalid definition: extraneous closing bracket");
}

whitelistClasses.add(new WhitelistClass(whitelistClassOrigin, javaClassName, noImport,
whitelistConstructors, whitelistMethods, whitelistFields));
// Create a new WhitelistClass with all the previously gathered constructors, methods,
// augmented methods, and fields, and add it to the list of whitelisted classes.
if ("class".equals(parseType)) {
whitelistClasses.add(new WhitelistClass(whitelistClassOrigin, javaClassName, noImport,
whitelistConstructors, whitelistMethods, whitelistFields));

whitelistClassOrigin = null;
javaClassName = null;
noImport = false;
whitelistConstructors = null;
whitelistMethods = null;
whitelistFields = null;
}

// Set all the variables to null to ensure a new class definition is found before other parsable values.
whitelistClassOrigin = null;
javaClassName = null;
noImport = false;
whitelistConstructors = null;
whitelistMethods = null;
whitelistFields = null;
// Reset the parseType.
parseType = null;

// Handle all other valid cases.
} else {
// Handle static definition types.
// Expects the following format: ID ID '(' ( ID ( ',' ID )* )? ')' 'bound_to' ID '\n'
} else if ("static".equals(parseType)) {
// Mark the origin of this parsable object.
String origin = "[" + filepath + "]:[" + number + "]";

// Parse the tokens prior to the method parameters.
int parameterStartIndex = line.indexOf('(');

if (parameterStartIndex == -1) {
throw new IllegalArgumentException(
"illegal static definition: start of method parameters not found [" + line + "]");
}

String[] tokens = line.substring(0, parameterStartIndex).trim().split("\\s+");

String methodName;

// Based on the number of tokens, look up the Java method name.
if (tokens.length == 2) {
methodName = tokens[1];
} else {
throw new IllegalArgumentException("invalid method definition: unexpected format [" + line + "]");
}

String returnCanonicalTypeName = tokens[0];

// Parse the method parameters.
int parameterEndIndex = line.indexOf(')');

if (parameterEndIndex == -1) {
throw new IllegalArgumentException(
"illegal static definition: end of method parameters not found [" + line + "]");
}

String[] canonicalTypeNameParameters =
line.substring(parameterStartIndex + 1, parameterEndIndex).replaceAll("\\s+", "").split(",");

// Handle the case for a method with no parameters.
if ("".equals(canonicalTypeNameParameters[0])) {
canonicalTypeNameParameters = new String[0];
}

// Parse the static type and class.
tokens = line.substring(parameterEndIndex + 1).trim().split("\\s+");

String staticType;
String targetJavaClassName;

// Based on the number of tokens, look up the type and class.
if (tokens.length == 2) {
staticType = tokens[0];
targetJavaClassName = tokens[1];
} else {
throw new IllegalArgumentException("invalid static definition: unexpected format [" + line + "]");
}

// Check the static type is valid.
if ("bound_to".equals(staticType) == false) {
throw new IllegalArgumentException(
"invalid static definition: unexpected static type [" + staticType + "] [" + line + "]");
}

whitelistBindings.add(new WhitelistBinding(origin, targetJavaClassName,
methodName, returnCanonicalTypeName, Arrays.asList(canonicalTypeNameParameters)));

// Handle class definition types.
} else if ("class".equals(parseType)) {
// Mark the origin of this parsable object.
String origin = "[" + filepath + "]:[" + number + "]";

// Ensure we have a defined class before adding any constructors, methods, augmented methods, or fields.
if (javaClassName == null) {
throw new IllegalArgumentException("invalid object definition: expected a class name [" + line + "]");
if (parseType == null) {
throw new IllegalArgumentException("invalid definition: expected one of ['class', 'static'] [" + line + "]");
}

// Handle the case for a constructor definition.
Expand All @@ -221,7 +311,7 @@ public static Whitelist loadFromResourceFiles(Class<?> resource, String... filep
// Ensure the final token of the line is ')'.
if (line.endsWith(")") == false) {
throw new IllegalArgumentException(
"invalid constructor definition: expected a closing parenthesis [" + line + "]");
"invalid constructor definition: expected a closing parenthesis [" + line + "]");
}

// Parse the constructor parameters.
Expand All @@ -234,34 +324,34 @@ public static Whitelist loadFromResourceFiles(Class<?> resource, String... filep

whitelistConstructors.add(new WhitelistConstructor(origin, Arrays.asList(tokens)));

// Handle the case for a method or augmented method definition.
// Expects the following format: ID ID? ID '(' ( ID ( ',' ID )* )? ')' '\n'
// Handle the case for a method or augmented method definition.
// Expects the following format: ID ID? ID '(' ( ID ( ',' ID )* )? ')' '\n'
} else if (line.contains("(")) {
// Ensure the final token of the line is ')'.
if (line.endsWith(")") == false) {
throw new IllegalArgumentException(
"invalid method definition: expected a closing parenthesis [" + line + "]");
"invalid method definition: expected a closing parenthesis [" + line + "]");
}

// Parse the tokens prior to the method parameters.
int parameterIndex = line.indexOf('(');
String[] tokens = line.trim().substring(0, parameterIndex).split("\\s+");
String[] tokens = line.substring(0, parameterIndex).trim().split("\\s+");

String javaMethodName;
String methodName;
String javaAugmentedClassName;

// Based on the number of tokens, look up the Java method name and if provided the Java augmented class.
if (tokens.length == 2) {
javaMethodName = tokens[1];
methodName = tokens[1];
javaAugmentedClassName = null;
} else if (tokens.length == 3) {
javaMethodName = tokens[2];
methodName = tokens[2];
javaAugmentedClassName = tokens[1];
} else {
throw new IllegalArgumentException("invalid method definition: unexpected format [" + line + "]");
}

String painlessReturnTypeName = tokens[0];
String returnCanonicalTypeName = tokens[0];

// Parse the method parameters.
tokens = line.substring(parameterIndex + 1, line.length() - 1).replaceAll("\\s+", "").split(",");
Expand All @@ -271,11 +361,11 @@ public static Whitelist loadFromResourceFiles(Class<?> resource, String... filep
tokens = new String[0];
}

whitelistMethods.add(new WhitelistMethod(origin, javaAugmentedClassName, javaMethodName,
painlessReturnTypeName, Arrays.asList(tokens)));
whitelistMethods.add(new WhitelistMethod(origin, javaAugmentedClassName, methodName,
returnCanonicalTypeName, Arrays.asList(tokens)));

// Handle the case for a field definition.
// Expects the following format: ID ID '\n'
// Handle the case for a field definition.
// Expects the following format: ID ID '\n'
} else {
// Parse the field tokens.
String[] tokens = line.split("\\s+");
Expand All @@ -287,20 +377,23 @@ public static Whitelist loadFromResourceFiles(Class<?> resource, String... filep

whitelistFields.add(new WhitelistField(origin, tokens[1], tokens[0]));
}
} else {
throw new IllegalArgumentException("invalid definition: unable to parse line [" + line + "]");
}
}

// Ensure all classes end with a '}' token before the end of the file.
if (javaClassName != null) {
throw new IllegalArgumentException("invalid class definition: expected closing bracket");
throw new IllegalArgumentException("invalid definition: expected closing bracket");
}
} catch (Exception exception) {
throw new RuntimeException("error in [" + filepath + "] at line [" + number + "]", exception);
}
}

ClassLoader loader = AccessController.doPrivileged((PrivilegedAction<ClassLoader>)resource::getClassLoader);

return new Whitelist(loader, whitelistClasses);
return new Whitelist(loader, whitelistClasses, whitelistBindings);
}

private WhitelistLoader() {}
Expand Down
Loading

0 comments on commit b52818e

Please sign in to comment.