/* * 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); } }