blob: 9df5d74ed8b7007bdb8f0fc8225c503bb4d17a62 [file] [log] [blame]
/*
* 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.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.text.ParseException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.apache.lucene.expressions.Expression;
import org.apache.lucene.util.LuceneTestCase;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.GeneratorAdapter;
/** Tests customing the function map */
public class TestCustomFunctions extends LuceneTestCase {
private static double DELTA = 0.0000001;
/** empty list of methods */
public void testEmpty() throws Exception {
Map<String,Method> functions = Collections.emptyMap();
ParseException expected = expectThrows(ParseException.class, () -> {
JavascriptCompiler.compile("sqrt(20)", functions, getClass().getClassLoader());
});
assertEquals("Invalid expression 'sqrt(20)': Unrecognized function call (sqrt).", expected.getMessage());
assertEquals(expected.getErrorOffset(), 0);
}
/** using the default map explicitly */
public void testDefaultList() throws Exception {
Map<String,Method> functions = JavascriptCompiler.DEFAULT_FUNCTIONS;
Expression expr = JavascriptCompiler.compile("sqrt(20)", functions, getClass().getClassLoader());
assertEquals(Math.sqrt(20), expr.evaluate(null), DELTA);
}
public static double zeroArgMethod() { return 5; }
/** tests a method with no arguments */
public void testNoArgMethod() throws Exception {
Map<String,Method> functions = new HashMap<>();
functions.put("foo", getClass().getMethod("zeroArgMethod"));
Expression expr = JavascriptCompiler.compile("foo()", functions, getClass().getClassLoader());
assertEquals(5, expr.evaluate(null), DELTA);
}
public static double oneArgMethod(double arg1) { return 3 + arg1; }
/** tests a method with one arguments */
public void testOneArgMethod() throws Exception {
Map<String,Method> functions = new HashMap<>();
functions.put("foo", getClass().getMethod("oneArgMethod", double.class));
Expression expr = JavascriptCompiler.compile("foo(3)", functions, getClass().getClassLoader());
assertEquals(6, expr.evaluate(null), DELTA);
}
public static double threeArgMethod(double arg1, double arg2, double arg3) { return arg1 + arg2 + arg3; }
/** tests a method with three arguments */
public void testThreeArgMethod() throws Exception {
Map<String,Method> functions = new HashMap<>();
functions.put("foo", getClass().getMethod("threeArgMethod", double.class, double.class, double.class));
Expression expr = JavascriptCompiler.compile("foo(3, 4, 5)", functions, getClass().getClassLoader());
assertEquals(12, expr.evaluate(null), DELTA);
}
/** tests a map with 2 functions */
public void testTwoMethods() throws Exception {
Map<String,Method> functions = new HashMap<>();
functions.put("foo", getClass().getMethod("zeroArgMethod"));
functions.put("bar", getClass().getMethod("oneArgMethod", double.class));
Expression expr = JavascriptCompiler.compile("foo() + bar(3)", functions, getClass().getClassLoader());
assertEquals(11, expr.evaluate(null), DELTA);
}
/** tests invalid methods that are not allowed to become variables to be mapped */
public void testInvalidVariableMethods() {
ParseException expected = expectThrows(ParseException.class, () -> {
JavascriptCompiler.compile("method()");
});
assertEquals("Invalid expression 'method()': Unrecognized function call (method).", expected.getMessage());
assertEquals(0, expected.getErrorOffset());
expected = expectThrows(ParseException.class, () -> {
JavascriptCompiler.compile("method.method(1)");
});
assertEquals("Invalid expression 'method.method(1)': Unrecognized function call (method.method).", expected.getMessage());
assertEquals(0, expected.getErrorOffset());
expected = expectThrows(ParseException.class, () -> {
JavascriptCompiler.compile("1 + method()");
});
assertEquals("Invalid expression '1 + method()': Unrecognized function call (method).", expected.getMessage());
assertEquals(4, expected.getErrorOffset());
}
public static String bogusReturnType() { return "bogus!"; }
/** wrong return type: must be double */
public void testWrongReturnType() throws Exception {
Map<String,Method> functions = new HashMap<>();
functions.put("foo", getClass().getMethod("bogusReturnType"));
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
JavascriptCompiler.compile("foo()", functions, getClass().getClassLoader());
});
assertTrue(expected.getMessage().contains("does not return a double"));
}
public static double bogusParameterType(String s) { return 0; }
/** wrong param type: must be doubles */
public void testWrongParameterType() throws Exception {
Map<String,Method> functions = new HashMap<>();
functions.put("foo", getClass().getMethod("bogusParameterType", String.class));
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
JavascriptCompiler.compile("foo(2)", functions, getClass().getClassLoader());
});
assertTrue(expected.getMessage().contains("must take only double parameters"));
}
public double nonStaticMethod() { return 0; }
/** wrong modifiers: must be static */
public void testWrongNotStatic() throws Exception {
Map<String,Method> functions = new HashMap<>();
functions.put("foo", getClass().getMethod("nonStaticMethod"));
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
JavascriptCompiler.compile("foo()", functions, getClass().getClassLoader());
});
assertTrue(expected.getMessage().contains("is not static"));
}
static double nonPublicMethod() { return 0; }
/** wrong modifiers: must be public */
public void testWrongNotPublic() throws Exception {
Map<String,Method> functions = new HashMap<>();
functions.put("foo", getClass().getDeclaredMethod("nonPublicMethod"));
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
JavascriptCompiler.compile("foo()", functions, getClass().getClassLoader());
});
assertTrue(expected.getMessage().contains("not public"));
}
static class NestedNotPublic {
public static double method() { return 0; }
}
/** wrong class modifiers: class containing method is not public */
public void testWrongNestedNotPublic() throws Exception {
Map<String,Method> functions = new HashMap<>();
functions.put("foo", NestedNotPublic.class.getMethod("method"));
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
JavascriptCompiler.compile("foo()", functions, getClass().getClassLoader());
});
assertTrue(expected.getMessage().contains("not public"));
}
/** Classloader that can be used to create a fake static class that has one method returning a static var */
static final class Loader extends ClassLoader implements Opcodes {
Loader(ClassLoader parent) {
super(parent);
}
public Class<?> createFakeClass() {
String className = TestCustomFunctions.class.getName() + "$Foo";
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
classWriter.visit(Opcodes.V1_5, ACC_PUBLIC | ACC_SUPER | ACC_FINAL | ACC_SYNTHETIC,
className.replace('.', '/'), null, Type.getInternalName(Object.class), null);
org.objectweb.asm.commons.Method m = org.objectweb.asm.commons.Method.getMethod("void <init>()");
GeneratorAdapter constructor = new GeneratorAdapter(ACC_PRIVATE | ACC_SYNTHETIC, m, null, null, classWriter);
constructor.loadThis();
constructor.loadArgs();
constructor.invokeConstructor(Type.getType(Object.class), m);
constructor.returnValue();
constructor.endMethod();
GeneratorAdapter gen = new GeneratorAdapter(ACC_STATIC | ACC_PUBLIC | ACC_SYNTHETIC,
org.objectweb.asm.commons.Method.getMethod("double bar()"), null, null, classWriter);
gen.push(2.0);
gen.returnValue();
gen.endMethod();
byte[] bc = classWriter.toByteArray();
return defineClass(className, bc, 0, bc.length);
}
}
/** uses this test with a different classloader and tries to
* register it using the default classloader, which should fail */
public void testClassLoader() throws Exception {
ClassLoader thisLoader = getClass().getClassLoader();
Loader childLoader = new Loader(thisLoader);
Class<?> fooClass = childLoader.createFakeClass();
Method barMethod = fooClass.getMethod("bar");
Map<String,Method> functions = Collections.singletonMap("bar", barMethod);
assertNotSame(thisLoader, fooClass.getClassLoader());
assertNotSame(thisLoader, barMethod.getDeclaringClass().getClassLoader());
// this should pass:
Expression expr = JavascriptCompiler.compile("bar()", functions, childLoader);
assertEquals(2.0, expr.evaluate(null), DELTA);
// use our classloader, not the foreign one, which should fail!
IllegalArgumentException expected = expectThrows(IllegalArgumentException.class, () -> {
JavascriptCompiler.compile("bar()", functions, thisLoader);
});
assertTrue(expected.getMessage().contains("is not declared by a class which is accessible by the given parent ClassLoader"));
// mix foreign and default functions
Map<String,Method> mixedFunctions = new HashMap<>(JavascriptCompiler.DEFAULT_FUNCTIONS);
mixedFunctions.putAll(functions);
expr = JavascriptCompiler.compile("bar()", mixedFunctions, childLoader);
assertEquals(2.0, expr.evaluate(null), DELTA);
expr = JavascriptCompiler.compile("sqrt(20)", mixedFunctions, childLoader);
assertEquals(Math.sqrt(20), expr.evaluate(null), DELTA);
// use our classloader, not the foreign one, which should fail!
expected = expectThrows(IllegalArgumentException.class, () -> {
JavascriptCompiler.compile("bar()", mixedFunctions, thisLoader);
});
assertTrue(expected.getMessage().contains("is not declared by a class which is accessible by the given parent ClassLoader"));
}
static String MESSAGE = "This should not happen but it happens";
public static class StaticThrowingException {
public static double method() { throw new ArithmeticException(MESSAGE); }
}
/** the method throws an exception. We should check the stack trace that it contains the source code of the expression as file name. */
public void testThrowingException() throws Exception {
Map<String,Method> functions = new HashMap<>();
functions.put("foo", StaticThrowingException.class.getMethod("method"));
String source = "3 * foo() / 5";
Expression expr = JavascriptCompiler.compile(source, functions, getClass().getClassLoader());
ArithmeticException expected = expectThrows(ArithmeticException.class, () -> {
expr.evaluate(null);
});
assertEquals(MESSAGE, expected.getMessage());
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
expected.printStackTrace(pw);
pw.flush();
assertTrue(sw.toString().contains("JavascriptCompiler$CompiledExpression.evaluate(" + source + ")"));
}
/** test that namespaces work with custom expressions. */
public void testNamespaces() throws Exception {
Map<String, Method> functions = new HashMap<>();
functions.put("foo.bar", getClass().getMethod("zeroArgMethod"));
String source = "foo.bar()";
Expression expr = JavascriptCompiler.compile(source, functions, getClass().getClassLoader());
assertEquals(5, expr.evaluate(null), DELTA);
}
}