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

org.apache.commons.jexl2.JexlEngine Maven / Gradle / Ivy

There is a newer version: 2.1.1
Show newest version
/*
 * 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.commons.jexl2;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.io.Reader;
import java.net.URL;
import java.net.URLConnection;
import java.lang.ref.SoftReference;
import java.lang.reflect.Constructor;
import java.util.Map;
import java.util.Set;
import java.util.Collections;
import java.util.Map.Entry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.apache.commons.jexl2.parser.ParseException;
import org.apache.commons.jexl2.parser.Parser;
import org.apache.commons.jexl2.parser.JexlNode;
import org.apache.commons.jexl2.parser.TokenMgrError;
import org.apache.commons.jexl2.parser.ASTJexlScript;

import org.apache.commons.jexl2.introspection.Uberspect;
import org.apache.commons.jexl2.introspection.UberspectImpl;
import org.apache.commons.jexl2.introspection.JexlMethod;

/**
 * 

* Creates and evaluates Expression and Script objects. * Determines the behavior of Expressions & Scripts during their evaluation with respect to: *

    *
  • Introspection, see {@link Uberspect}
  • *
  • Arithmetic & comparison, see {@link JexlArithmetic}
  • *
  • Error reporting
  • *
  • Logging
  • *
*

*

The setSilentandsetLenient methods allow to fine-tune an engine instance behavior * according to various error control needs. *

*
    *
  • When "silent" & "lenient" (not-strict): *

    0 & null should be indicators of "default" values so that even in an case of error, * something meaningfull can still be inferred; may be convenient for configurations. *

    *
  • *
  • When "silent" & "strict": *

    One should probably consider using null as an error case - ie, every object * manipulated by JEXL should be valued; the ternary operator, especially the '?:' form * can be used to workaround exceptional cases. * Use case could be configuration with no implicit values or defaults. *

    *
  • *
  • When "not-silent" & "not-strict": *

    The error control grain is roughly on par with JEXL 1.0

    *
  • *
  • When "not-silent" & "strict": *

    The finest error control grain is obtained; it is the closest to Java code - * still augmented by "script" capabilities regarding automated conversions & type matching. *

    *
  • *
*

* Note that methods that evaluate expressions may throw unchecked exceptions; * The {@link JexlException} are thrown in "non-silent" mode but since these are * RuntimeException, user-code should catch them wherever most appropriate. *

* @since 2.0 */ public class JexlEngine { /** * An empty/static/non-mutable JexlContext used instead of null context. */ public static final JexlContext EMPTY_CONTEXT = new JexlContext() { /** {@inheritDoc} */ public Object get(String name) { return null; } /** {@inheritDoc} */ public boolean has(String name) { return false; } /** {@inheritDoc} */ public void set(String name, Object value) { throw new UnsupportedOperationException("Not supported in void context."); } }; /** * Gets the default instance of Uberspect. *

This is lazily initialized to avoid building a default instance if there * is no use for it. The main reason for not using the default Uberspect instance is to * be able to use a (low level) introspector created with a given logger * instead of the default one.

*

Implemented as on demand holder idiom.

*/ private static final class UberspectHolder { /** The default uberspector that handles all introspection patterns. */ private static final Uberspect UBERSPECT = new UberspectImpl(LogFactory.getLog(JexlEngine.class)); /** Non-instantiable. */ private UberspectHolder() {} } /** * The Uberspect instance. */ protected final Uberspect uberspect; /** * The JexlArithmetic instance. */ protected final JexlArithmetic arithmetic; /** * The Log to which all JexlEngine messages will be logged. */ protected final Log logger; /** * The singleton ExpressionFactory also holds a single instance of * {@link Parser}. * When parsing expressions, ExpressionFactory synchronizes on Parser. */ protected final Parser parser = new Parser(new StringReader(";")); //$NON-NLS-1$ /** * Whether expressions evaluated by this engine will throw exceptions (false) or * return null (true). Default is false. */ protected boolean silent = false; /** * Whether error messages will carry debugging information. */ protected boolean debug = true; /** * The map of 'prefix:function' to object implementing the function. */ protected Map functions = Collections.emptyMap(); /** * The expression cache. */ protected SoftCache cache = null; /** * The default cache load factor. */ private static final float LOAD_FACTOR = 0.75f; /** * Creates an engine with default arguments. */ public JexlEngine() { this(null, null, null, null); } /** * Creates a JEXL engine using the provided {@link Uberspect}, (@link JexlArithmetic), * a function map and logger. * @param anUberspect to allow different introspection behaviour * @param anArithmetic to allow different arithmetic behaviour * @param theFunctions an optional map of functions (@link setFunctions) * @param log the logger for various messages */ public JexlEngine(Uberspect anUberspect, JexlArithmetic anArithmetic, Map theFunctions, Log log) { this.uberspect = anUberspect == null ? getUberspect(log) : anUberspect; if (log == null) { log = LogFactory.getLog(JexlEngine.class); } this.logger = log; this.arithmetic = anArithmetic == null ? new JexlArithmetic(true) : anArithmetic; if (theFunctions != null) { this.functions = theFunctions; } } /** * Gets the default instance of Uberspect. *

This is lazily initialized to avoid building a default instance if there * is no use for it. The main reason for not using the default Uberspect instance is to * be able to use a (low level) introspector created with a given logger * instead of the default one.

* @param logger the logger to use for the underlying Uberspect * @return Uberspect the default uberspector instance. */ public static Uberspect getUberspect(Log logger) { if (logger == null || logger.equals(LogFactory.getLog(JexlEngine.class))) { return UberspectHolder.UBERSPECT; } return new UberspectImpl(logger); } /** * Gets this engine underlying uberspect. * @return the uberspect */ public Uberspect getUberspect() { return uberspect; } /** * Sets whether this engine reports debugging information when error occurs. *

This method is not thread safe; it should be called as an optional step of the JexlEngine * initialization code before expression creation & evaluation.

* @see JexlEngine#setSilent * @see JexlEngine#setLenient * @param flag true implies debug is on, false implies debug is off. */ public void setDebug(boolean flag) { this.debug = flag; } /** * Checks whether this engine is in debug mode. * @return true if debug is on, false otherwise */ public boolean isDebug() { return this.debug; } /** * Sets whether this engine throws JexlException during evaluation when an error is triggered. *

This method is not thread safe; it should be called as an optional step of the JexlEngine * initialization code before expression creation & evaluation.

* @see JexlEngine#setDebug * @see JexlEngine#setLenient * @param flag true means no JexlException will occur, false allows them */ public void setSilent(boolean flag) { this.silent = flag; } /** * Checks whether this engine throws JexlException during evaluation. * @return true if silent, false (default) otherwise */ public boolean isSilent() { return this.silent; } /** * Sets whether this engine triggers errors during evaluation when null is used as * an operand. *

This method is not thread safe; it should be called as an optional step of the JexlEngine * initialization code before expression creation & evaluation.

* @see JexlEngine#setSilent * @see JexlEngine#setDebug * @param flag true means no JexlException will occur, false allows them */ public void setLenient(boolean flag) { this.arithmetic.setLenient(flag); } /** * Checks whether this engine triggers errors during evaluation when null is used as * an operand. * @return true if lenient, false if strict */ public boolean isLenient() { return this.arithmetic.isLenient(); } /** * Sets the class loader used to discover classes in 'new' expressions. *

This method should be called as an optional step of the JexlEngine * initialization code before expression creation & evaluation.

* @param loader the class loader to use */ public void setClassLoader(ClassLoader loader) { uberspect.setClassLoader(loader); } /** * Sets a cache of the defined size for expressions. * @param size if not strictly positive, no cache is used. */ public void setCache(int size) { // since the cache is only used during parse, use same sync object synchronized (parser) { if (size <= 0) { cache = null; } else if (cache == null || cache.size() != size) { cache = new SoftCache(size); } } } /** * Sets the map of function namespaces. *

* This method is not thread safe; it should be called as an optional step of the JexlEngine * initialization code before expression creation & evaluation. *

*

* Each entry key is used as a prefix, each entry value used as a bean implementing * methods; an expression like 'nsx:method(123)' will thus be solved by looking at * a registered bean named 'nsx' that implements method 'method' in that map. * If all methods are static, you may use the bean class instead of an instance as value. *

*

* If the entry value is a class that has one contructor taking a JexlContext as argument, an instance * of the namespace will be created at evaluation time. It might be a good idea to derive a JexlContext * to carry the information used by the namespace to avoid variable space pollution and strongly type * the constructor with this specialized JexlContext. *

*

* The key or prefix allows to retrieve the bean that plays the role of the namespace. * If the prefix is null, the namespace is the top-level namespace allowing to define * top-level user defined functions ( ie: myfunc(...) ) *

* @param funcs the map of functions that should not mutate after the call; if null * is passed, the empty collection is used. */ public void setFunctions(Map funcs) { functions = funcs != null ? funcs : Collections.emptyMap(); } /** * Retrieves the map of function namespaces. * * @return the map passed in setFunctions or the empty map if the * original was null. */ public Map getFunctions() { return functions; } /** * An overridable through covariant return Expression creator. * @param text the script text * @param tree the parse AST tree * @return the script instance */ protected Expression createExpression(ASTJexlScript tree, String text) { return new ExpressionImpl(this, text, tree); } /** * Creates an Expression from a String containing valid * JEXL syntax. This method parses the expression which * must contain either a reference or an expression. * @param expression A String containing valid JEXL syntax * @return An Expression object which can be evaluated with a JexlContext * @throws JexlException An exception can be thrown if there is a problem * parsing this expression, or if the expression is neither an * expression nor a reference. */ public Expression createExpression(String expression) { return createExpression(expression, null); } /** * Creates an Expression from a String containing valid * JEXL syntax. This method parses the expression which * must contain either a reference or an expression. * @param expression A String containing valid JEXL syntax * @return An Expression object which can be evaluated with a JexlContext * @param info An info structure to carry debugging information if needed * @throws JexlException An exception can be thrown if there is a problem * parsing this expression, or if the expression is neither an * expression or a reference. */ public Expression createExpression(String expression, JexlInfo info) { // Parse the expression ASTJexlScript tree = parse(expression, info); if (tree.jjtGetNumChildren() > 1) { logger.warn("The JEXL Expression created will be a reference" + " to the first expression from the supplied script: \"" + expression + "\" "); } return createExpression(tree, expression); } /** * Creates a Script from a String containing valid JEXL syntax. * This method parses the script which validates the syntax. * * @param scriptText A String containing valid JEXL syntax * @return A {@link Script} which can be executed using a {@link JexlContext}. * @throws JexlException if there is a problem parsing the script. */ public Script createScript(String scriptText) { return createScript(scriptText, null); } /** * Creates a Script from a String containing valid JEXL syntax. * This method parses the script which validates the syntax. * * @param scriptText A String containing valid JEXL syntax * @param info An info structure to carry debugging information if needed * @return A {@link Script} which can be executed using a {@link JexlContext}. * @throws JexlException if there is a problem parsing the script. */ public Script createScript(String scriptText, JexlInfo info) { if (scriptText == null) { throw new NullPointerException("scriptText is null"); } // Parse the expression ASTJexlScript tree = parse(scriptText, info); return createScript(tree, scriptText); } /** * An overridable through covariant return Script creator. * @param text the script text * @param tree the parse AST tree * @return the script instance */ protected Script createScript(ASTJexlScript tree, String text) { return new ExpressionImpl(this, text, tree); } /** * Creates a Script from a {@link File} containing valid JEXL syntax. * This method parses the script and validates the syntax. * * @param scriptFile A {@link File} containing valid JEXL syntax. * Must not be null. Must be a readable file. * @return A {@link Script} which can be executed with a * {@link JexlContext}. * @throws IOException if there is a problem reading the script. * @throws JexlException if there is a problem parsing the script. */ public Script createScript(File scriptFile) throws IOException { if (scriptFile == null) { throw new NullPointerException("scriptFile is null"); } if (!scriptFile.canRead()) { throw new IOException("Can't read scriptFile (" + scriptFile.getCanonicalPath() + ")"); } BufferedReader reader = new BufferedReader(new FileReader(scriptFile)); JexlInfo info = null; if (debug) { info = createInfo(scriptFile.getName(), 0, 0); } return createScript(readerToString(reader), info); } /** * Creates a Script from a {@link URL} containing valid JEXL syntax. * This method parses the script and validates the syntax. * * @param scriptUrl A {@link URL} containing valid JEXL syntax. * Must not be null. Must be a readable file. * @return A {@link Script} which can be executed with a * {@link JexlContext}. * @throws IOException if there is a problem reading the script. * @throws JexlException if there is a problem parsing the script. */ public Script createScript(URL scriptUrl) throws IOException { if (scriptUrl == null) { throw new NullPointerException("scriptUrl is null"); } URLConnection connection = scriptUrl.openConnection(); BufferedReader reader = new BufferedReader( new InputStreamReader(connection.getInputStream())); JexlInfo info = null; if (debug) { info = createInfo(scriptUrl.toString(), 0, 0); } return createScript(readerToString(reader), info); } /** * Accesses properties of a bean using an expression. *

* jexl.get(myobject, "foo.bar"); should equate to * myobject.getFoo().getBar(); (or myobject.getFoo().get("bar")) *

*

* If the JEXL engine is silent, errors will be logged through its logger as warning. *

* @param bean the bean to get properties from * @param expr the property expression * @return the value of the property * @throws JexlException if there is an error parsing the expression or during evaluation */ public Object getProperty(Object bean, String expr) { return getProperty(null, bean, expr); } /** * Accesses properties of a bean using an expression. *

* If the JEXL engine is silent, errors will be logged through its logger as warning. *

* @param context the evaluation context * @param bean the bean to get properties from * @param expr the property expression * @return the value of the property * @throws JexlException if there is an error parsing the expression or during evaluation */ public Object getProperty(JexlContext context, Object bean, String expr) { if (context == null) { context = EMPTY_CONTEXT; } // lets build 1 unique & unused identifiers wrt context String r0 = "$0"; for (int s = 0; context.has(r0); ++s) { r0 = r0 + s; } expr = r0 + (expr.charAt(0) == '[' ? "" : ".") + expr + ";"; try { JexlNode tree = parse(expr, null); JexlNode node = tree.jjtGetChild(0); Interpreter interpreter = createInterpreter(context); // ensure 4 objects in register array Object[] r = {r0, bean, r0, bean}; interpreter.setRegisters(r); return node.jjtAccept(interpreter, null); } catch (JexlException xjexl) { if (silent) { logger.warn(xjexl.getMessage(), xjexl.getCause()); return null; } throw xjexl; } } /** * Assign properties of a bean using an expression. *

* jexl.set(myobject, "foo.bar", 10); should equate to * myobject.getFoo().setBar(10); (or myobject.getFoo().put("bar", 10) ) *

*

* If the JEXL engine is silent, errors will be logged through its logger as warning. *

* @param bean the bean to set properties in * @param expr the property expression * @param value the value of the property * @throws JexlException if there is an error parsing the expression or during evaluation */ public void setProperty(Object bean, String expr, Object value) { setProperty(null, bean, expr, value); } /** * Assign properties of a bean using an expression. *

* If the JEXL engine is silent, errors will be logged through its logger as warning. *

* @param context the evaluation context * @param bean the bean to set properties in * @param expr the property expression * @param value the value of the property * @throws JexlException if there is an error parsing the expression or during evaluation */ public void setProperty(JexlContext context, Object bean, String expr, Object value) { if (context == null) { context = EMPTY_CONTEXT; } // lets build 2 unique & unused identifiers wrt context String r0 = "$0", r1 = "$1"; for (int s = 0; context.has(r0); ++s) { r0 = r0 + s; } for (int s = 0; context.has(r1); ++s) { r1 = r1 + s; } // synthetize expr expr = r0 + (expr.charAt(0) == '[' ? "" : ".") + expr + "=" + r1 + ";"; try { JexlNode tree = parse(expr, null); JexlNode node = tree.jjtGetChild(0); Interpreter interpreter = createInterpreter(context); // set the registers Object[] r = {r0, bean, r1, value}; interpreter.setRegisters(r); node.jjtAccept(interpreter, null); } catch (JexlException xjexl) { if (silent) { logger.warn(xjexl.getMessage(), xjexl.getCause()); return; } throw xjexl; } } /** * Invokes an object's method by name and arguments. * @param obj the method's invoker object * @param meth the method's name * @param args the method's arguments * @return the method returned value or null if it failed and engine is silent * @throws JexlException if method could not be found or failed and engine is not silent */ public Object invokeMethod(Object obj, String meth, Object... args) { JexlException xjexl = null; Object result = null; JexlInfo info = debugInfo(); try { JexlMethod method = uberspect.getMethod(obj, meth, args, info); if (method == null && arithmetic.narrowArguments(args)) { method = uberspect.getMethod(obj, meth, args, info); } if (method != null) { result = method.invoke(obj, args); } else { xjexl = new JexlException(info, "failed finding method " + meth); } } catch (Exception xany) { xjexl = new JexlException(info, "failed executing method " + meth, xany); } finally { if (xjexl != null) { if (silent) { logger.warn(xjexl.getMessage(), xjexl.getCause()); return null; } throw xjexl; } } return result; } /** * Creates a new instance of an object using the most appropriate constructor * based on the arguments. * @param the type of object * @param clazz the class to instantiate * @param args the constructor arguments * @return the created object instance or null on failure when silent */ public T newInstance(Class clazz, Object...args) { return clazz.cast(doCreateInstance(clazz, args)); } /** * Creates a new instance of an object using the most appropriate constructor * based on the arguments. * @param clazz the name of the class to instantiate resolved through this engine's class loader * @param args the constructor arguments * @return the created object instance or null on failure when silent */ public Object newInstance(String clazz, Object...args) { return doCreateInstance(clazz, args); } /** * Creates a new instance of an object using the most appropriate constructor * based on the arguments. * @param clazz the class to instantiate * @param args the constructor arguments * @return the created object instance or null on failure when silent */ protected Object doCreateInstance(Object clazz, Object...args) { JexlException xjexl = null; Object result = null; JexlInfo info = debugInfo(); try { Constructor ctor = uberspect.getConstructor(clazz, args, info); if (ctor == null && arithmetic.narrowArguments(args)) { ctor = uberspect.getConstructor(clazz, args, info); } if (ctor != null) { result = ctor.newInstance(args); } else { xjexl = new JexlException(info, "failed finding constructor for " + clazz.toString()); } } catch (Exception xany) { xjexl = new JexlException(info, "failed executing constructor for " + clazz.toString(), xany); } finally { if (xjexl != null) { if (silent) { logger.warn(xjexl.getMessage(), xjexl.getCause()); return null; } throw xjexl; } } return result; } /** * Creates an interpreter. * @param context a JexlContext; if null, the EMPTY_CONTEXT is used instead. * @return an Interpreter */ protected Interpreter createInterpreter(JexlContext context) { if (context == null) { context = EMPTY_CONTEXT; } return new Interpreter(this, context); } /** * A soft reference on cache. *

The cache is held through a soft reference, allowing it to be GCed under * memory pressure.

* @param the cache key entry type * @param the cache key value type */ protected class SoftCache { /** * The cache size. */ private final int size; /** * The soft reference to the cache map. */ private SoftReference> ref = null; /** * Creates a new instance of a soft cache. * @param theSize the cache size */ SoftCache(int theSize) { size = theSize; } /** * Returns the cache size. * @return the cache size */ int size() { return size; } /** * Produces the cache entry set. * @return the cache entry set */ Set> entrySet() { Map map = ref != null ? ref.get() : null; return map != null ? map.entrySet() : Collections.>emptySet(); } /** * Gets a value from cache. * @param key the cache entry key * @return the cache entry value */ V get(K key) { final Map map = ref != null ? ref.get() : null; return map != null ? map.get(key) : null; } /** * Puts a value in cache. * @param key the cache entry key * @param script the cache entry value */ void put(K key, V script) { Map map = ref != null ? ref.get() : null; if (map == null) { map = createCache(size); ref = new SoftReference>(map); } map.put(key, script); } } /** * Creates a cache. * @param the key type * @param the value type * @param cacheSize the cache size, must be > 0 * @return a Map usable as a cache bounded to the given size */ protected Map createCache(final int cacheSize) { return new java.util.LinkedHashMap(cacheSize, LOAD_FACTOR, true) { /** Serial version UID. */ private static final long serialVersionUID = 3801124242820219131L; @Override protected boolean removeEldestEntry(Map.Entry eldest) { return size() > cacheSize; } }; } /** * Parses an expression. * @param expression the expression to parse * @param info debug information structure * @return the parsed tree * @throws JexlException if any error occured during parsing */ protected ASTJexlScript parse(CharSequence expression, JexlInfo info) { String expr = cleanExpression(expression); ASTJexlScript tree = null; synchronized (parser) { if (cache != null) { tree = cache.get(expr); if (tree != null) { return tree; } } try { Reader reader = new StringReader(expr); // use first calling method of JexlEngine as debug info if (info == null) { info = debugInfo(); } tree = parser.parse(reader, info); if (cache != null) { cache.put(expr, tree); } } catch (TokenMgrError xtme) { throw new JexlException(info, "tokenization failed", xtme); } catch (ParseException xparse) { throw new JexlException(info, "parsing failed", xparse); } } return tree; } /** * Creates a JexlInfo instance. * @param fn url/file name * @param l line number * @param c column number * @return a JexlInfo instance */ protected JexlInfo createInfo(String fn, int l, int c) { return new DebugInfo(fn, l, c); } /** * Creates and fills up debugging information. *

This gathers the class, method and line number of the first calling method * not owned by JexlEngine, UnifiedJEXL or {Script,Expression}Factory.

* @return an Info if debug is set, null otherwise */ protected JexlInfo debugInfo() { JexlInfo info = null; if (debug) { Throwable xinfo = new Throwable(); xinfo.fillInStackTrace(); StackTraceElement[] stack = xinfo.getStackTrace(); StackTraceElement se = null; Class clazz = getClass(); for (int s = 1; s < stack.length; ++s, se = null) { se = stack[s]; String className = se.getClassName(); if (!className.equals(clazz.getName())) { // go deeper if called from JexlEngine or UnifiedJEXL if (className.equals(JexlEngine.class.getName())) { clazz = JexlEngine.class; } else if (className.equals(UnifiedJEXL.class.getName())) { clazz = UnifiedJEXL.class; } else { break; } } } if (se != null) { info = createInfo(se.getClassName() + "." + se.getMethodName(), se.getLineNumber(), 0); } } return info; } /** * Trims the expression from front & ending spaces. * @param str expression to clean * @return trimmed expression ending in a semi-colon */ public static final String cleanExpression(CharSequence str) { if (str != null) { int start = 0; int end = str.length(); if (end > 0) { // trim front spaces while (start < end && str.charAt(start) == ' ') { ++start; } // trim ending spaces while (end > 0 && str.charAt(end - 1) == ' ') { --end; } return str.subSequence(start, end).toString(); } return ""; } return null; } /** * Read from a reader into a StringBuffer and return a String with * the contents of the reader. * @param scriptReader to be read. * @return the contents of the reader as a String. * @throws IOException on any error reading the reader. */ public static final String readerToString(Reader scriptReader) throws IOException { StringBuilder buffer = new StringBuilder(); BufferedReader reader; if (scriptReader instanceof BufferedReader) { reader = (BufferedReader) scriptReader; } else { reader = new BufferedReader(scriptReader); } try { String line; while ((line = reader.readLine()) != null) { buffer.append(line).append('\n'); } return buffer.toString(); } finally { try { reader.close(); } catch(IOException xio) { // ignore } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy