/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF 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.apache.lucene.expressions.js;

import java.io.IOException;
import java.io.Reader;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;

import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTree;
import org.apache.lucene.expressions.Expression;
import org.apache.lucene.expressions.js.JavascriptParser.ExpressionContext;
import org.apache.lucene.search.DoubleValues;
import org.apache.lucene.util.IOUtils;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.GeneratorAdapter;

An expression compiler for javascript expressions.

Example:

  Expression foo = JavascriptCompiler.compile("((0.3*popularity)/10.0)+(0.7*score)");

See the package documentation for the supported syntax and default functions.

You can compile with an alternate set of functions via compile(String, Map<String,Method>, ClassLoader). For example:

  Map<String,Method> functions = new HashMap<>();
  // add all the default functions
  functions.putAll(JavascriptCompiler.DEFAULT_FUNCTIONS);
  // add cbrt()
  functions.put("cbrt", Math.class.getMethod("cbrt", double.class));
  // call compile with customized function map
  Expression foo = JavascriptCompiler.compile("cbrt(score)+ln(popularity)", 
                                              functions, 
                                              getClass().getClassLoader());
@lucene.experimental
/** * An expression compiler for javascript expressions. * <p> * Example: * <pre class="prettyprint"> * Expression foo = JavascriptCompiler.compile("((0.3*popularity)/10.0)+(0.7*score)"); * </pre> * <p> * See the {@link org.apache.lucene.expressions.js package documentation} for * the supported syntax and default functions. * <p> * You can compile with an alternate set of functions via {@link #compile(String, Map, ClassLoader)}. * For example: * <pre class="prettyprint"> * Map&lt;String,Method&gt; functions = new HashMap&lt;&gt;(); * // add all the default functions * functions.putAll(JavascriptCompiler.DEFAULT_FUNCTIONS); * // add cbrt() * functions.put("cbrt", Math.class.getMethod("cbrt", double.class)); * // call compile with customized function map * Expression foo = JavascriptCompiler.compile("cbrt(score)+ln(popularity)", * functions, * getClass().getClassLoader()); * </pre> * * @lucene.experimental */
public final class JavascriptCompiler { static final class Loader extends ClassLoader { Loader(ClassLoader parent) { super(parent); } public Class<? extends Expression> define(String className, byte[] bytecode) { return defineClass(className, bytecode, 0, bytecode.length).asSubclass(Expression.class); } } private static final int CLASSFILE_VERSION = Opcodes.V1_8; // We use the same class name for all generated classes as they all have their own class loader. // The source code is displayed as "source file name" in stack trace. private static final String COMPILED_EXPRESSION_CLASS = JavascriptCompiler.class.getName() + "$CompiledExpression"; private static final String COMPILED_EXPRESSION_INTERNAL = COMPILED_EXPRESSION_CLASS.replace('.', '/'); static final Type EXPRESSION_TYPE = Type.getType(Expression.class); static final Type FUNCTION_VALUES_TYPE = Type.getType(DoubleValues.class); private static final org.objectweb.asm.commons.Method EXPRESSION_CTOR = getAsmMethod(void.class, "<init>", String.class, String[].class), EVALUATE_METHOD = getAsmMethod(double.class, "evaluate", DoubleValues[].class); static final org.objectweb.asm.commons.Method DOUBLE_VAL_METHOD = getAsmMethod(double.class, "doubleValue");
create an ASM Method object from return type, method name, and parameters.
/** create an ASM Method object from return type, method name, and parameters. */
private static org.objectweb.asm.commons.Method getAsmMethod(Class<?> rtype, String name, Class<?>... ptypes) { return new org.objectweb.asm.commons.Method(name, MethodType.methodType(rtype, ptypes).toMethodDescriptorString()); } // This maximum length is theoretically 65535 bytes, but as it's CESU-8 encoded we dont know how large it is in bytes, so be safe // rcmuir: "If your ranking function is that large you need to check yourself into a mental institution!" private static final int MAX_SOURCE_LENGTH = 16384; final String sourceText; final Map<String,Method> functions;
Compiles the given expression.
Params:
  • sourceText – The expression to compile
Throws:
Returns:A new compiled expression
/** * Compiles the given expression. * * @param sourceText The expression to compile * @return A new compiled expression * @throws ParseException on failure to compile */
public static Expression compile(String sourceText) throws ParseException { return new JavascriptCompiler(sourceText).compileExpression(JavascriptCompiler.class.getClassLoader()); }
Compiles the given expression with the supplied custom functions.

Functions must be public static, return double and can take from zero to 256 double parameters.

Params:
  • sourceText – The expression to compile
  • functions – map of String names to functions
  • parent – a ClassLoader that should be used as the parent of the loaded class. It must contain all classes referred to by the given functions.
Throws:
Returns:A new compiled expression
/** * Compiles the given expression with the supplied custom functions. * <p> * Functions must be {@code public static}, return {@code double} and * can take from zero to 256 {@code double} parameters. * * @param sourceText The expression to compile * @param functions map of String names to functions * @param parent a {@code ClassLoader} that should be used as the parent of the loaded class. * It must contain all classes referred to by the given {@code functions}. * @return A new compiled expression * @throws ParseException on failure to compile */
public static Expression compile(String sourceText, Map<String,Method> functions, ClassLoader parent) throws ParseException { if (parent == null) { throw new NullPointerException("A parent ClassLoader must be given."); } for (Method m : functions.values()) { checkFunctionClassLoader(m, parent); checkFunction(m); } return new JavascriptCompiler(sourceText, functions).compileExpression(parent); }
This method is unused, it is just here to make sure that the function signatures don't change. If this method fails to compile, you also have to change the byte code generator to correctly use the FunctionValues class.
/** * This method is unused, it is just here to make sure that the function signatures don't change. * If this method fails to compile, you also have to change the byte code generator to correctly * use the FunctionValues class. */
@SuppressWarnings({"unused", "null"}) private static void unusedTestCompile() throws IOException { DoubleValues f = null; double ret = f.doubleValue(); }
Constructs a compiler for expressions.
Params:
  • sourceText – The expression to compile
/** * Constructs a compiler for expressions. * @param sourceText The expression to compile */
private JavascriptCompiler(String sourceText) { this(sourceText, DEFAULT_FUNCTIONS); }
Constructs a compiler for expressions with specific set of functions
Params:
  • sourceText – The expression to compile
/** * Constructs a compiler for expressions with specific set of functions * @param sourceText The expression to compile */
private JavascriptCompiler(String sourceText, Map<String,Method> functions) { if (sourceText == null) { throw new NullPointerException(); } this.sourceText = sourceText; this.functions = functions; }
Compiles the given expression with the specified parent classloader
Throws:
Returns:A new compiled expression
/** * Compiles the given expression with the specified parent classloader * * @return A new compiled expression * @throws ParseException on failure to compile */
private Expression compileExpression(ClassLoader parent) throws ParseException { final Map<String, Integer> externalsMap = new LinkedHashMap<>(); final ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); try { generateClass(getAntlrParseTree(), classWriter, externalsMap); final Class<? extends Expression> evaluatorClass = new Loader(parent) .define(COMPILED_EXPRESSION_CLASS, classWriter.toByteArray()); final Constructor<? extends Expression> constructor = evaluatorClass.getConstructor(String.class, String[].class); return constructor.newInstance(sourceText, externalsMap.keySet().toArray(new String[externalsMap.size()])); } catch (RuntimeException re) { if (re.getCause() instanceof ParseException) { throw (ParseException)re.getCause(); } throw re; } catch (ReflectiveOperationException exception) { throw new IllegalStateException("An internal error occurred attempting to compile the expression (" + sourceText + ").", exception); } }
Parses the sourceText into an ANTLR 4 parse tree
Throws:
Returns:The ANTLR parse tree
/** * Parses the sourceText into an ANTLR 4 parse tree * * @return The ANTLR parse tree * @throws ParseException on failure to parse */
private ParseTree getAntlrParseTree() throws ParseException { final ANTLRInputStream antlrInputStream = new ANTLRInputStream(sourceText); final JavascriptErrorHandlingLexer javascriptLexer = new JavascriptErrorHandlingLexer(antlrInputStream); javascriptLexer.removeErrorListeners(); final JavascriptParser javascriptParser = new JavascriptParser(new CommonTokenStream(javascriptLexer)); javascriptParser.removeErrorListeners(); javascriptParser.setErrorHandler(new JavascriptParserErrorStrategy()); return javascriptParser.compile(); }
Sends the bytecode of class file to ClassWriter.
/** * Sends the bytecode of class file to {@link ClassWriter}. */
private void generateClass(final ParseTree parseTree, final ClassWriter classWriter, final Map<String, Integer> externalsMap) throws ParseException { classWriter.visit(CLASSFILE_VERSION, Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER | Opcodes.ACC_FINAL, COMPILED_EXPRESSION_INTERNAL, null, EXPRESSION_TYPE.getInternalName(), null); final String clippedSourceText = (sourceText.length() <= MAX_SOURCE_LENGTH) ? sourceText : (sourceText.substring(0, MAX_SOURCE_LENGTH - 3) + "..."); classWriter.visitSource(clippedSourceText, null); final GeneratorAdapter constructor = new GeneratorAdapter(Opcodes.ACC_PUBLIC, EXPRESSION_CTOR, null, null, classWriter); constructor.loadThis(); constructor.loadArgs(); constructor.invokeConstructor(EXPRESSION_TYPE, EXPRESSION_CTOR); constructor.returnValue(); constructor.endMethod(); final GeneratorAdapter gen = new GeneratorAdapter(Opcodes.ACC_PUBLIC, EVALUATE_METHOD, null, null, classWriter); // to completely hide the ANTLR visitor we use an anonymous impl: new JavascriptBaseVisitor<Void>() { private final Deque<Type> typeStack = new ArrayDeque<>(); @Override public Void visitCompile(JavascriptParser.CompileContext ctx) { typeStack.push(Type.DOUBLE_TYPE); visit(ctx.expression()); typeStack.pop(); return null; } @Override public Void visitPrecedence(JavascriptParser.PrecedenceContext ctx) { visit(ctx.expression()); return null; } @Override public Void visitNumeric(JavascriptParser.NumericContext ctx) { if (ctx.HEX() != null) { pushLong(Long.parseLong(ctx.HEX().getText().substring(2), 16)); } else if (ctx.OCTAL() != null) { pushLong(Long.parseLong(ctx.OCTAL().getText().substring(1), 8)); } else if (ctx.DECIMAL() != null) { gen.push(Double.parseDouble(ctx.DECIMAL().getText())); gen.cast(Type.DOUBLE_TYPE, typeStack.peek()); } else { throw new IllegalStateException("Unknown operation specified: " + ctx.getText()); } return null; } @Override public Void visitExternal(JavascriptParser.ExternalContext ctx) { String text = ctx.VARIABLE().getText(); int arguments = ctx.expression().size(); boolean parens = ctx.LP() != null && ctx.RP() != null; Method method = parens ? functions.get(text) : null; try { if (method != null) { int arity = method.getParameterTypes().length; if (arguments != arity) { throw new ParseException( "Invalid expression '" + sourceText + "': Expected (" + arity + ") arguments for function call (" + text + "), but found (" + arguments + ").", ctx.start.getStartIndex()); } typeStack.push(Type.DOUBLE_TYPE); for (int argument = 0; argument < arguments; ++argument) { visit(ctx.expression(argument)); } typeStack.pop(); gen.invokeStatic(Type.getType(method.getDeclaringClass()), org.objectweb.asm.commons.Method.getMethod(method)); gen.cast(Type.DOUBLE_TYPE, typeStack.peek()); } else if (!parens || arguments == 0 && text.contains(".")) { int index; text = normalizeQuotes(ctx.getText()); if (externalsMap.containsKey(text)) { index = externalsMap.get(text); } else { index = externalsMap.size(); externalsMap.put(text, index); } gen.loadArg(0); gen.push(index); gen.arrayLoad(FUNCTION_VALUES_TYPE); gen.invokeVirtual(FUNCTION_VALUES_TYPE, DOUBLE_VAL_METHOD); gen.cast(Type.DOUBLE_TYPE, typeStack.peek()); } else { throw new ParseException("Invalid expression '" + sourceText + "': Unrecognized function call (" + text + ").", ctx.start.getStartIndex()); } return null; } catch (ParseException e) { // The API doesn't allow checked exceptions here, so propagate up the stack. This is unwrapped // in getAntlrParseTree. throw new RuntimeException(e); } } @Override public Void visitUnary(JavascriptParser.UnaryContext ctx) { if (ctx.BOOLNOT() != null) { Label labelNotTrue = new Label(); Label labelNotReturn = new Label(); typeStack.push(Type.INT_TYPE); visit(ctx.expression()); typeStack.pop(); gen.visitJumpInsn(Opcodes.IFEQ, labelNotTrue); pushBoolean(false); gen.goTo(labelNotReturn); gen.visitLabel(labelNotTrue); pushBoolean(true); gen.visitLabel(labelNotReturn); } else if (ctx.BWNOT() != null) { typeStack.push(Type.LONG_TYPE); visit(ctx.expression()); typeStack.pop(); gen.push(-1L); gen.visitInsn(Opcodes.LXOR); gen.cast(Type.LONG_TYPE, typeStack.peek()); } else if (ctx.ADD() != null) { visit(ctx.expression()); } else if (ctx.SUB() != null) { typeStack.push(Type.DOUBLE_TYPE); visit(ctx.expression()); typeStack.pop(); gen.visitInsn(Opcodes.DNEG); gen.cast(Type.DOUBLE_TYPE, typeStack.peek()); } else { throw new IllegalStateException("Unknown operation specified: " + ctx.getText()); } return null; } @Override public Void visitMuldiv(JavascriptParser.MuldivContext ctx) { int opcode; if (ctx.MUL() != null) { opcode = Opcodes.DMUL; } else if (ctx.DIV() != null) { opcode = Opcodes.DDIV; } else if (ctx.REM() != null) { opcode = Opcodes.DREM; } else { throw new IllegalStateException("Unknown operation specified: " + ctx.getText()); } pushArith(opcode, ctx.expression(0), ctx.expression(1)); return null; } @Override public Void visitAddsub(JavascriptParser.AddsubContext ctx) { int opcode; if (ctx.ADD() != null) { opcode = Opcodes.DADD; } else if (ctx.SUB() != null) { opcode = Opcodes.DSUB; } else { throw new IllegalStateException("Unknown operation specified: " + ctx.getText()); } pushArith(opcode, ctx.expression(0), ctx.expression(1)); return null; } @Override public Void visitBwshift(JavascriptParser.BwshiftContext ctx) { int opcode; if (ctx.LSH() != null) { opcode = Opcodes.LSHL; } else if (ctx.RSH() != null) { opcode = Opcodes.LSHR; } else if (ctx.USH() != null) { opcode = Opcodes.LUSHR; } else { throw new IllegalStateException("Unknown operation specified: " + ctx.getText()); } pushShift(opcode, ctx.expression(0), ctx.expression(1)); return null; } @Override public Void visitBoolcomp(JavascriptParser.BoolcompContext ctx) { int opcode; if (ctx.LT() != null) { opcode = GeneratorAdapter.LT; } else if (ctx.LTE() != null) { opcode = GeneratorAdapter.LE; } else if (ctx.GT() != null) { opcode = GeneratorAdapter.GT; } else if (ctx.GTE() != null) { opcode = GeneratorAdapter.GE; } else { throw new IllegalStateException("Unknown operation specified: " + ctx.getText()); } pushCond(opcode, ctx.expression(0), ctx.expression(1)); return null; } @Override public Void visitBooleqne(JavascriptParser.BooleqneContext ctx) { int opcode; if (ctx.EQ() != null) { opcode = GeneratorAdapter.EQ; } else if (ctx.NE() != null) { opcode = GeneratorAdapter.NE; } else { throw new IllegalStateException("Unknown operation specified: " + ctx.getText()); } pushCond(opcode, ctx.expression(0), ctx.expression(1)); return null; } @Override public Void visitBwand(JavascriptParser.BwandContext ctx) { pushBitwise(Opcodes.LAND, ctx.expression(0), ctx.expression(1)); return null; } @Override public Void visitBwxor(JavascriptParser.BwxorContext ctx) { pushBitwise(Opcodes.LXOR, ctx.expression(0), ctx.expression(1)); return null; } @Override public Void visitBwor(JavascriptParser.BworContext ctx) { pushBitwise(Opcodes.LOR, ctx.expression(0), ctx.expression(1)); return null; } @Override public Void visitBooland(JavascriptParser.BoolandContext ctx) { Label andFalse = new Label(); Label andEnd = new Label(); typeStack.push(Type.INT_TYPE); visit(ctx.expression(0)); gen.visitJumpInsn(Opcodes.IFEQ, andFalse); visit(ctx.expression(1)); gen.visitJumpInsn(Opcodes.IFEQ, andFalse); typeStack.pop(); pushBoolean(true); gen.goTo(andEnd); gen.visitLabel(andFalse); pushBoolean(false); gen.visitLabel(andEnd); return null; } @Override public Void visitBoolor(JavascriptParser.BoolorContext ctx) { Label orTrue = new Label(); Label orEnd = new Label(); typeStack.push(Type.INT_TYPE); visit(ctx.expression(0)); gen.visitJumpInsn(Opcodes.IFNE, orTrue); visit(ctx.expression(1)); gen.visitJumpInsn(Opcodes.IFNE, orTrue); typeStack.pop(); pushBoolean(false); gen.goTo(orEnd); gen.visitLabel(orTrue); pushBoolean(true); gen.visitLabel(orEnd); return null; } @Override public Void visitConditional(JavascriptParser.ConditionalContext ctx) { Label condFalse = new Label(); Label condEnd = new Label(); typeStack.push(Type.INT_TYPE); visit(ctx.expression(0)); typeStack.pop(); gen.visitJumpInsn(Opcodes.IFEQ, condFalse); visit(ctx.expression(1)); gen.goTo(condEnd); gen.visitLabel(condFalse); visit(ctx.expression(2)); gen.visitLabel(condEnd); return null; } private void pushArith(int operator, ExpressionContext left, ExpressionContext right) { pushBinaryOp(operator, left, right, Type.DOUBLE_TYPE, Type.DOUBLE_TYPE, Type.DOUBLE_TYPE); } private void pushShift(int operator, ExpressionContext left, ExpressionContext right) { pushBinaryOp(operator, left, right, Type.LONG_TYPE, Type.INT_TYPE, Type.LONG_TYPE); } private void pushBitwise(int operator, ExpressionContext left, ExpressionContext right) { pushBinaryOp(operator, left, right, Type.LONG_TYPE, Type.LONG_TYPE, Type.LONG_TYPE); } private void pushBinaryOp(int operator, ExpressionContext left, ExpressionContext right, Type leftType, Type rightType, Type returnType) { typeStack.push(leftType); visit(left); typeStack.pop(); typeStack.push(rightType); visit(right); typeStack.pop(); gen.visitInsn(operator); gen.cast(returnType, typeStack.peek()); } private void pushCond(int operator, ExpressionContext left, ExpressionContext right) { Label labelTrue = new Label(); Label labelReturn = new Label(); typeStack.push(Type.DOUBLE_TYPE); visit(left); visit(right); typeStack.pop(); gen.ifCmp(Type.DOUBLE_TYPE, operator, labelTrue); pushBoolean(false); gen.goTo(labelReturn); gen.visitLabel(labelTrue); pushBoolean(true); gen.visitLabel(labelReturn); } private void pushBoolean(boolean truth) { switch (typeStack.peek().getSort()) { case Type.INT: gen.push(truth); break; case Type.LONG: gen.push(truth ? 1L : 0L); break; case Type.DOUBLE: gen.push(truth ? 1. : 0.); break; default: throw new IllegalStateException("Invalid expected type: " + typeStack.peek()); } } private void pushLong(long i) { switch (typeStack.peek().getSort()) { case Type.INT: gen.push((int) i); break; case Type.LONG: gen.push(i); break; case Type.DOUBLE: gen.push((double) i); break; default: throw new IllegalStateException("Invalid expected type: " + typeStack.peek()); } } }.visit(parseTree); gen.returnValue(); gen.endMethod(); classWriter.visitEnd(); } static String normalizeQuotes(String text) { StringBuilder out = new StringBuilder(text.length()); boolean inDoubleQuotes = false; for (int i = 0; i < text.length(); ++i) { char c = text.charAt(i); if (c == '\\') { c = text.charAt(++i); if (c == '\\') { out.append('\\'); // re-escape the backslash } // no escape for double quote } else if (c == '\'') { if (inDoubleQuotes) { // escape in output out.append('\\'); } else { int j = findSingleQuoteStringEnd(text, i); out.append(text, i, j); // copy up to end quote (leave end for append below) i = j; } } else if (c == '"') { c = '\''; // change beginning/ending doubles to singles inDoubleQuotes = !inDoubleQuotes; } out.append(c); } return out.toString(); } static int findSingleQuoteStringEnd(String text, int start) { ++start; // skip beginning while (text.charAt(start) != '\'') { if (text.charAt(start) == '\\') { ++start; // blindly consume escape value } ++start; } return start; }
The default set of functions available to expressions.

See the package documentation for a list.

/** * The default set of functions available to expressions. * <p> * See the {@link org.apache.lucene.expressions.js package documentation} * for a list. */
public static final Map<String,Method> DEFAULT_FUNCTIONS; static { Map<String,Method> map = new HashMap<>(); try { final Properties props = new Properties(); try (Reader in = IOUtils.getDecodingReader(JavascriptCompiler.class, JavascriptCompiler.class.getSimpleName() + ".properties", StandardCharsets.UTF_8)) { props.load(in); } for (final String call : props.stringPropertyNames()) { final String[] vals = props.getProperty(call).split(","); if (vals.length != 3) { throw new Error("Syntax error while reading Javascript functions from resource"); } final Class<?> clazz = Class.forName(vals[0].trim()); final String methodName = vals[1].trim(); final int arity = Integer.parseInt(vals[2].trim()); @SuppressWarnings({"rawtypes", "unchecked"}) Class[] args = new Class[arity]; Arrays.fill(args, double.class); Method method = clazz.getMethod(methodName, args); checkFunction(method); map.put(call, method); } } catch (ReflectiveOperationException | IOException e) { throw new Error("Cannot resolve function", e); } DEFAULT_FUNCTIONS = Collections.unmodifiableMap(map); }
Check Method signature for compatibility.
/** Check Method signature for compatibility. */
private static void checkFunction(Method method) { // check that the Method is public in some public reachable class: final MethodType type; try { type = MethodHandles.publicLookup().unreflect(method).type(); } catch (IllegalAccessException iae) { throw new IllegalArgumentException(method + " is not accessible (declaring class or method not public)."); } // do some checks if the signature is "compatible": if (!Modifier.isStatic(method.getModifiers())) { throw new IllegalArgumentException(method + " is not static."); } for (int arg = 0, arity = type.parameterCount(); arg < arity; arg++) { if (type.parameterType(arg) != double.class) { throw new IllegalArgumentException(method + " must take only double parameters."); } } if (type.returnType() != double.class) { throw new IllegalArgumentException(method + " does not return a double."); } }
Cross check if declaring class of given method is the same as returned by the given parent ClassLoader on string lookup. This prevents NoClassDefFoundError.
/** Cross check if declaring class of given method is the same as * returned by the given parent {@link ClassLoader} on string lookup. * This prevents {@link NoClassDefFoundError}. */
private static void checkFunctionClassLoader(Method method, ClassLoader parent) { boolean ok = false; try { final Class<?> clazz = method.getDeclaringClass(); ok = Class.forName(clazz.getName(), false, parent) == clazz; } catch (ClassNotFoundException e) { ok = false; } if (!ok) { throw new IllegalArgumentException(method + " is not declared by a class which is accessible by the given parent ClassLoader."); } } }