org.apache.commons.jexl2.JexlEngine Maven / Gradle / Ivy
Show all versions of commons-jexl Show documentation
/*
* 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 setSilent
andsetLenient
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 extends T> 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