From 4963e64f9b5a5cb15a28b967a32ed13040b89e43 Mon Sep 17 00:00:00 2001 From: Robert Muir Date: Sat, 14 Sep 2013 18:29:53 +0000 Subject: [PATCH] LUCENE-5207: make functions pluggable git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/branches/lucene5207@1523286 13f79535-47bb-0310-9956-ffa450edef68 --- .../expressions/js/JavascriptCompiler.java | 102 +++++++++++++++++- .../expressions/js/JavascriptFunction.java | 98 ----------------- ...operties => JavascriptCompiler.properties} | 0 3 files changed, 97 insertions(+), 103 deletions(-) delete mode 100644 lucene/expressions/src/java/org/apache/lucene/expressions/js/JavascriptFunction.java rename lucene/expressions/src/resources/org/apache/lucene/expressions/js/{JavascriptFunction.properties => JavascriptCompiler.properties} (100%) diff --git a/lucene/expressions/src/java/org/apache/lucene/expressions/js/JavascriptCompiler.java b/lucene/expressions/src/java/org/apache/lucene/expressions/js/JavascriptCompiler.java index 83bed209975..bb3531eb7fe 100644 --- a/lucene/expressions/src/java/org/apache/lucene/expressions/js/JavascriptCompiler.java +++ b/lucene/expressions/src/java/org/apache/lucene/expressions/js/JavascriptCompiler.java @@ -16,13 +16,19 @@ package org.apache.lucene.expressions.js; * limitations under the License. */ +import java.io.IOException; +import java.io.Reader; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.text.ParseException; -import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; +import java.util.Properties; import org.antlr.runtime.ANTLRStringStream; import org.antlr.runtime.CharStream; @@ -31,6 +37,7 @@ import org.antlr.runtime.RecognitionException; import org.antlr.runtime.tree.Tree; import org.apache.lucene.expressions.Expression; import org.apache.lucene.queries.function.FunctionValues; +import org.apache.lucene.util.IOUtils; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; @@ -144,6 +151,8 @@ public class JavascriptCompiler { private final ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); private MethodVisitor methodVisitor; + private final Map functions; + /** * Compiles the given expression. * @@ -156,7 +165,24 @@ public class JavascriptCompiler { } /** - * This method is unused, it is just here to make sure that the funcion signatures don't change. + * Compiles the given expression with the supplied custom functions. + *

+ * Functions must return a double. + * + * @param sourceText The expression to compile + * @param functions map of String names to functions + * @return A new compiled expression + * @throws ParseException on failure to compile + */ + public static Expression compile(String sourceText, Map functions) throws ParseException { + for (Method m : functions.values()) { + checkFunction(m); + } + return new JavascriptCompiler(sourceText, functions).compileExpression(); + } + + /** + * 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. */ @@ -171,10 +197,19 @@ public class JavascriptCompiler { * @param sourceText The expression to compile */ private JavascriptCompiler(String sourceText) { + this(sourceText, DEFAULT_FUNCTIONS); + } + + /** + * Constructs a compiler for expressions with specific set of functions + * @param sourceText The expression to compile + */ + private JavascriptCompiler(String sourceText, Map functions) { if (sourceText == null) { throw new NullPointerException(); } this.sourceText = sourceText; + this.functions = functions; } /** @@ -230,13 +265,25 @@ public class JavascriptCompiler { String call = identifier.getText(); int arguments = current.getChildCount() - 1; - JavascriptFunction method = JavascriptFunction.getMethod(call, arguments); + Method method = functions.get(call); + if (method == null) { + throw new IllegalArgumentException("Unrecognized method call (" + call + ")."); + } + + int arity = method.getParameterTypes().length; + if (arguments != arity && arity != -1) { + throw new IllegalArgumentException("Expected (" + arity + ") arguments for method call (" + + call + "), but found (" + arguments + ")."); + } for (int argument = 1; argument <= arguments; ++argument) { recursiveCompile(current.getChild(argument), ComputedType.DOUBLE); } - methodVisitor.visitMethodInsn(INVOKESTATIC, method.klass, method.method, method.descriptor); + String klass = Type.getInternalName(method.getDeclaringClass()); + String name = method.getName(); + String descriptor = Type.getMethodDescriptor(method); + methodVisitor.visitMethodInsn(INVOKESTATIC, klass, name, descriptor); typeCompile(expected, ComputedType.DOUBLE); break; @@ -624,4 +671,49 @@ public class JavascriptCompiler { throw exception; } } + + /** + * The default set of functions available to expressions. + *

+ * See the {@link org.apache.lucene.expressions.js package documentation} + * for a list. + */ + public static final Map DEFAULT_FUNCTIONS; + static { + Map map = new HashMap(); + try { + final Properties props = new Properties(); + try (Reader in = IOUtils.getDecodingReader(JavascriptCompiler.class, + JavascriptCompiler.class.getSimpleName() + ".properties", IOUtils.CHARSET_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 (NoSuchMethodException | ClassNotFoundException | IOException e) { + throw new Error("Cannot resolve function", e); + } + DEFAULT_FUNCTIONS = Collections.unmodifiableMap(map); + } + + /* do some checks if the signature is "compatible" */ + private static void checkFunction(Method method) { + if (!Modifier.isStatic(method.getModifiers())) { + throw new IllegalArgumentException(method + " is not static."); + } + if (method.getReturnType() != double.class) { + throw new IllegalArgumentException(method + " does not return a double."); + } + } } diff --git a/lucene/expressions/src/java/org/apache/lucene/expressions/js/JavascriptFunction.java b/lucene/expressions/src/java/org/apache/lucene/expressions/js/JavascriptFunction.java deleted file mode 100644 index 11d034ef201..00000000000 --- a/lucene/expressions/src/java/org/apache/lucene/expressions/js/JavascriptFunction.java +++ /dev/null @@ -1,98 +0,0 @@ -package org.apache.lucene.expressions.js; -/* - * 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. - */ - -import java.io.IOException; -import java.io.Reader; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.Properties; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; - -import org.apache.lucene.util.IOUtils; -import org.apache.lucene.util.MathUtil; -import org.objectweb.asm.Type; - -/** - * A wrapper to delegate function calls from a javascript expression. - */ -class JavascriptFunction { - - private static final Map methods = new HashMap(); - static { - try { - final Properties props = new Properties(); - try (Reader in = IOUtils.getDecodingReader(JavascriptFunction.class, - JavascriptFunction.class.getSimpleName() + ".properties", IOUtils.CHARSET_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); - methods.put(call, new JavascriptFunction(call, clazz.getMethod(methodName, args))); - } - } catch (NoSuchMethodException | ClassNotFoundException | IOException e) { - throw new Error("Cannot resolve function", e); - } - } - - public static JavascriptFunction getMethod(String call, int arity) { - JavascriptFunction method = methods.get(call); - - if (method == null) { - throw new IllegalArgumentException("Unrecognized method call (" + call + ")."); - } - - if (arity != method.arity && method.arity != -1) { - throw new IllegalArgumentException("Expected (" + method.arity + ") arguments for method call (" + - call + "), but found (" + arity + ")."); - } - - return method; - } - - public final String call; - public final int arity; - public final String klass; - public final String method; - public final String descriptor; - - private JavascriptFunction(String call, Method method) { - // do some checks if the signature is "compatible": - if (!Modifier.isStatic(method.getModifiers())) { - throw new IllegalArgumentException(method + " is not static."); - } - if (method.getReturnType() != double.class) { - throw new IllegalArgumentException(method + " does not return a double."); - } - - this.call = call; - this.arity = method.getParameterTypes().length; - this.klass = Type.getInternalName(method.getDeclaringClass()); - this.method = method.getName(); - this.descriptor = Type.getMethodDescriptor(method); - } -} diff --git a/lucene/expressions/src/resources/org/apache/lucene/expressions/js/JavascriptFunction.properties b/lucene/expressions/src/resources/org/apache/lucene/expressions/js/JavascriptCompiler.properties similarity index 100% rename from lucene/expressions/src/resources/org/apache/lucene/expressions/js/JavascriptFunction.properties rename to lucene/expressions/src/resources/org/apache/lucene/expressions/js/JavascriptCompiler.properties