All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.elasticsearch.painless.spi.WhitelistLoader Maven / Gradle / Ivy

/*
 * 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.io.InputStreamReader;
import java.io.LineNumberReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/** Loads and creates a {@link Whitelist} from one to many text files. */
public final class WhitelistLoader {

    /**
     * Loads and creates a {@link Whitelist} from one to many text files. The file paths are passed in as an array of
     * {@link String}s with a single {@link Class} to be be used to load the resources where each {@link String}
     * is the path of a single text file. The {@link Class}'s {@link ClassLoader} will be used to lookup the Java
     * reflection objects for each individual {@link Class}, {@link Constructor}, {@link Method}, and {@link Field}
     * specified as part of the whitelist in the text file.
     *
     * A single pass is made through each file to collect all the information about each class, constructor, method,
     * and field. Most validation will be done at a later point after all whitelists have been gathered and their
     * merging takes place.
     *
     * A painless type name is one of the following:
     * 
    *
  • def - The Painless dynamic type which is automatically included without a need to be * whitelisted.
  • *
  • fully-qualified Java type name - Any whitelisted Java class will have the equivalent name as * a Painless type name with the exception that any dollar symbols used as part of inner classes will * be replaced with dot symbols.
  • *
  • short Java type name - The text after the final dot symbol of any specified Java class. A * short type Java name may be excluded by using the 'no_import' token during Painless class parsing * as described later.
  • *
* * The following can be parsed from each whitelist text file: *
    *
  • Blank lines will be ignored by the parser.
  • *
  • Comments may be created starting with a pound '#' symbol and end with a newline. These will * be ignored by the parser.
  • *
  • Primitive types may be specified starting with 'class' and followed by the Java type name, * an opening bracket, a newline, a closing bracket, and a final newline.
  • *
  • Complex types may be specified starting with 'class' and followed the fully-qualified Java * class name, optionally followed by an 'no_import' token, an opening bracket, a newline, * constructor/method/field specifications, a closing bracket, and a final newline. Within a complex * type the following may be parsed: *
      *
    • A constructor may be specified starting with an opening parenthesis, followed by a * comma-delimited list of Painless type names corresponding to the type/class names for * the equivalent Java parameter types (these must be whitelisted as well), a closing * parenthesis, and a newline.
    • *
    • A method may be specified starting with a Painless type name for the return type, * followed by the Java name of the method (which will also be the Painless name for the * method), an opening parenthesis, a comma-delimited list of Painless type names * corresponding to the type/class names for the equivalent Java parameter types * (these must be whitelisted as well), a closing parenthesis, and a newline.
    • *
    • An augmented method may be specified starting with a Painless type name for the return * type, followed by the fully qualified Java name of the class the augmented method is * part of (this class does not need to be whitelisted), the Java name of the method * (which will also be the Painless name for the method), an opening parenthesis, a * comma-delimited list of Painless type names corresponding to the type/class names * for the equivalent Java parameter types (these must be whitelisted as well), a closing * parenthesis, and a newline.
    • *
    • A field may be specified starting with a Painless type name for the equivalent Java type * of the field, followed by the Java name of the field (which all be the Painless name * for the field), and a newline.
    • *
    *
* * Note there must be a one-to-one correspondence of Painless type names to Java type/class names. * If the same Painless type is defined across multiple files and the Java class is the same, all * specified constructors, methods, and fields will be merged into a single Painless type. The * Painless dynamic type, 'def', used as part of constructor, method, and field definitions will * be appropriately parsed and handled. Painless complex types must be specified with the * fully-qualified Java class name. Method argument types, method return types, and field types * must be specified with Painless type names (def, fully-qualified, or short) as described earlier. * * The following example is used to create a single whitelist text file: * * {@code * # primitive types * * class int -> int { * } * * # complex types * * class my.package.Example no_import { * # constructors * () * (int) * (def, def) * (Example, def) * * # method * Example add(int, def) * int add(Example, Example) * void example() * * # augmented * Example some.other.Class sub(Example, int, def) * * # fields * int value0 * int value1 * def value2 * } * } */ public static Whitelist loadFromResourceFiles(Class resource, String... filepaths) { List whitelistClasses = new ArrayList<>(); List whitelistStatics = new ArrayList<>(); List whitelistClassBindings = 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. for (String filepath : filepaths) { String line; int number = -1; try (LineNumberReader reader = new LineNumberReader( new InputStreamReader(resource.getResourceAsStream(filepath), StandardCharsets.UTF_8))) { String parseType = null; String whitelistClassOrigin = null; String javaClassName = null; boolean noImport = false; List whitelistConstructors = null; List whitelistMethods = null; List whitelistFields = null; while ((line = reader.readLine()) != null) { number = reader.getLineNumber(); line = line.trim(); // Skip any lines that are either blank or comments. if (line.length() == 0 || line.charAt(0) == '#') { continue; } // Handle a new class by resetting all the variables necessary to construct a new WhitelistClass for the whitelist. // Expects the following format: 'class' ID 'no_import'? '{' '\n' if (line.startsWith("class ")) { // 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 + "]"); } if (parseType != null) { throw new IllegalArgumentException("invalid definition: cannot embed class definition [" + line + "]"); } // Parse the Java class name. String[] tokens = line.substring(5, line.length() - 1).trim().split("\\s+"); // Ensure the correct number of tokens. if (tokens.length == 2 && "no_import".equals(tokens[1])) { noImport = true; } else if (tokens.length != 1) { 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_import ")) { // Ensure the final token of the line is '{'. if (line.endsWith("{") == false) { throw new IllegalArgumentException( "invalid static import definition: failed to parse static import opening bracket [" + line + "]"); } if (parseType != null) { throw new IllegalArgumentException("invalid definition: cannot embed static import definition [" + line + "]"); } parseType = "static_import"; // Handle the end of a definition and reset all previously gathered values. // Expects the following format: '}' '\n' } else if (line.equals("}")) { if (parseType == null) { throw new IllegalArgumentException("invalid definition: extraneous closing bracket"); } // Create a new WhitelistClass with all the previously gathered constructors, methods, // augmented methods, and fields, and add it to the list of whitelisted classes. if ("class".equals(parseType)) { whitelistClasses.add(new WhitelistClass(whitelistClassOrigin, javaClassName, noImport, whitelistConstructors, whitelistMethods, whitelistFields)); whitelistClassOrigin = null; javaClassName = null; noImport = false; whitelistConstructors = null; whitelistMethods = null; whitelistFields = null; } // Reset the parseType. parseType = null; // Handle static import definition types. // Expects the following format: ID ID '(' ( ID ( ',' ID )* )? ')' ( 'from_class' | 'bound_to' ) ID '\n' } else if ("static_import".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 import 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 import 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 import type and class. tokens = line.substring(parameterEndIndex + 1).trim().split("\\s+"); String staticImportType; String targetJavaClassName; // Based on the number of tokens, look up the type and class. if (tokens.length == 2) { staticImportType = tokens[0]; targetJavaClassName = tokens[1]; } else { throw new IllegalArgumentException("invalid static import definition: unexpected format [" + line + "]"); } // Add a static import method or binding depending on the static import type. if ("from_class".equals(staticImportType)) { whitelistStatics.add(new WhitelistMethod(origin, targetJavaClassName, methodName, returnCanonicalTypeName, Arrays.asList(canonicalTypeNameParameters))); } else if ("bound_to".equals(staticImportType)) { whitelistClassBindings.add(new WhitelistClassBinding(origin, targetJavaClassName, methodName, returnCanonicalTypeName, Arrays.asList(canonicalTypeNameParameters))); } else { throw new IllegalArgumentException("invalid static import definition: " + "unexpected static import type [" + staticImportType + "] [" + line + "]"); } // Handle class definition types. } else if ("class".equals(parseType)) { // Mark the origin of this parsable object. String origin = "[" + filepath + "]:[" + number + "]"; // Handle the case for a constructor definition. // Expects the following format: '(' ( ID ( ',' ID )* )? ')' '\n' if (line.startsWith("(")) { // Ensure the final token of the line is ')'. if (line.endsWith(")") == false) { throw new IllegalArgumentException( "invalid constructor definition: expected a closing parenthesis [" + line + "]"); } // Parse the constructor parameters. String[] tokens = line.substring(1, line.length() - 1).replaceAll("\\s+", "").split(","); // Handle the case for a constructor with no parameters. if ("".equals(tokens[0])) { tokens = new String[0]; } 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' } 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 + "]"); } // Parse the tokens prior to the method parameters. int parameterIndex = line.indexOf('('); String[] tokens = line.substring(0, parameterIndex).trim().split("\\s+"); 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) { methodName = tokens[1]; javaAugmentedClassName = null; } else if (tokens.length == 3) { methodName = tokens[2]; javaAugmentedClassName = tokens[1]; } else { throw new IllegalArgumentException("invalid method definition: unexpected format [" + line + "]"); } String returnCanonicalTypeName = tokens[0]; // Parse the method parameters. tokens = line.substring(parameterIndex + 1, line.length() - 1).replaceAll("\\s+", "").split(","); // Handle the case for a method with no parameters. if ("".equals(tokens[0])) { tokens = new String[0]; } 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' } else { // Parse the field tokens. String[] tokens = line.split("\\s+"); // Ensure the correct number of tokens. if (tokens.length != 2) { throw new IllegalArgumentException("invalid field definition: unexpected format [" + line + "]"); } 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 definition: expected closing bracket"); } } catch (Exception exception) { throw new RuntimeException("error in [" + filepath + "] at line [" + number + "]", exception); } } ClassLoader loader = AccessController.doPrivileged((PrivilegedAction)resource::getClassLoader); return new Whitelist(loader, whitelistClasses, whitelistStatics, whitelistClassBindings); } private WhitelistLoader() {} }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy