blob: 368d773ee2452984753a43783556f7f138d0c57a [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.commons.jexl3.internal;
import org.apache.commons.jexl3.JexlArithmetic;
import org.apache.commons.jexl3.JexlBuilder;
import org.apache.commons.jexl3.JexlContext;
import org.apache.commons.jexl3.JexlEngine;
import org.apache.commons.jexl3.JexlException;
import org.apache.commons.jexl3.JexlInfo;
import org.apache.commons.jexl3.JexlScript;
import org.apache.commons.jexl3.internal.introspection.SandboxUberspect;
import org.apache.commons.jexl3.internal.introspection.Uberspect;
import org.apache.commons.jexl3.introspection.JexlMethod;
import org.apache.commons.jexl3.introspection.JexlSandbox;
import org.apache.commons.jexl3.introspection.JexlUberspect;
import org.apache.commons.jexl3.parser.ASTArrayAccess;
import org.apache.commons.jexl3.parser.ASTFunctionNode;
import org.apache.commons.jexl3.parser.ASTIdentifier;
import org.apache.commons.jexl3.parser.ASTIdentifierAccess;
import org.apache.commons.jexl3.parser.ASTJexlScript;
import org.apache.commons.jexl3.parser.ASTMethodNode;
import org.apache.commons.jexl3.parser.JexlNode;
import org.apache.commons.jexl3.parser.Parser;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.lang.ref.SoftReference;
import java.nio.charset.Charset;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* A JexlEngine implementation.
* @since 2.0
*/
public class Engine extends JexlEngine {
/**
* Gets the default instance of Uberspect.
* <p>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.</p>
* <p>Implemented as on demand holder idiom.</p>
*/
private static final class UberspectHolder {
/** The default uberspector that handles all introspection patterns. */
private static final Uberspect UBERSPECT =
new Uberspect(LogFactory.getLog(JexlEngine.class), JexlUberspect.JEXL_STRATEGY);
/** Non-instantiable. */
private UberspectHolder() {}
}
/**
* The JexlUberspect instance.
*/
protected final JexlUberspect uberspect;
/**
* The {@link JexlArithmetic} instance.
*/
protected final JexlArithmetic arithmetic;
/**
* The Log to which all JexlEngine messages will be logged.
*/
protected final Log logger;
/**
* The {@link Parser}; when parsing expressions, this engine synchronizes on the parser.
*/
protected final Parser parser = new Parser(new StringReader(";")); //$NON-NLS-1$
/**
* Whether this engine considers unknown variables, methods and constructors as errors.
*/
protected final boolean strict;
/**
* Whether expressions evaluated by this engine will throw exceptions (false) or return null (true) on errors.
* Default is false.
*/
protected final boolean silent;
/**
* Whether expressions evaluated by this engine will throw JexlException.Cancel (true) or return null (false) when
* interrupted.
* Default is true when not silent and strict.
*/
protected final boolean cancellable;
/**
* Whether error messages will carry debugging information.
*/
protected final boolean debug;
/**
* The map of 'prefix:function' to object implementing the namespaces.
*/
protected final Map<String, Object> functions;
/**
* The expression cache.
*/
protected final SoftCache<String, ASTJexlScript> cache;
/**
* The expression max length to hit the cache.
*/
protected final int cacheThreshold;
/**
* The default charset.
*/
protected final Charset charset;
/**
* The default jxlt engine.
*/
protected volatile TemplateEngine jxlt = null;
/**
* The default cache load factor.
*/
private static final float LOAD_FACTOR = 0.75f;
/**
* Creates an engine with default arguments.
*/
public Engine() {
this(new JexlBuilder());
}
/**
* Creates a JEXL engine using the provided {@link JexlBuilder}.
* @param conf the builder
*/
public Engine(JexlBuilder conf) {
JexlSandbox sandbox = conf.sandbox();
JexlUberspect uber = conf.uberspect() == null ? getUberspect(conf.logger(), conf.strategy()) : conf.uberspect();
ClassLoader loader = conf.loader();
if (loader != null) {
uber.setClassLoader(loader);
}
if (sandbox == null) {
this.uberspect = uber;
} else {
this.uberspect = new SandboxUberspect(uber, sandbox);
}
this.logger = conf.logger() == null ? LogFactory.getLog(JexlEngine.class) : conf.logger();
this.functions = conf.namespaces() == null ? Collections.<String, Object>emptyMap() : conf.namespaces();
this.strict = conf.strict() == null ? true : conf.strict();
this.silent = conf.silent() == null ? false : conf.silent();
this.cancellable = conf.cancellable() == null ? !silent && strict : conf.cancellable();
this.debug = conf.debug() == null ? true : conf.debug();
this.arithmetic = conf.arithmetic() == null ? new JexlArithmetic(this.strict) : conf.arithmetic();
this.cache = conf.cache() <= 0 ? null : new SoftCache<String, ASTJexlScript>(conf.cache());
this.cacheThreshold = conf.cacheThreshold();
this.charset = conf.charset();
if (uberspect == null) {
throw new IllegalArgumentException("uberspect can not be null");
}
}
/**
* Gets the default instance of Uberspect.
* <p>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.</p>
* @param logger the logger to use for the underlying Uberspect
* @param strategy the property resolver strategy
* @return Uberspect the default uberspector instance.
*/
public static Uberspect getUberspect(Log logger, JexlUberspect.ResolverStrategy strategy) {
if ((logger == null || logger.equals(LogFactory.getLog(JexlEngine.class)))
&& (strategy == null || strategy == JexlUberspect.JEXL_STRATEGY)) {
return UberspectHolder.UBERSPECT;
}
return new Uberspect(logger, strategy);
}
@Override
public JexlUberspect getUberspect() {
return uberspect;
}
@Override
public JexlArithmetic getArithmetic() {
return arithmetic;
}
@Override
public boolean isDebug() {
return this.debug;
}
@Override
public boolean isSilent() {
return this.silent;
}
@Override
public boolean isStrict() {
return strict;
}
@Override
public void setClassLoader(ClassLoader loader) {
uberspect.setClassLoader(loader);
}
@Override
public Charset getCharset() {
return charset;
}
@Override
public TemplateEngine createJxltEngine(boolean noScript, int cacheSize, char immediate, char deferred) {
return new TemplateEngine(this, noScript, cacheSize, immediate, deferred);
}
/**
* Swaps the current thread local context.
* @param tls the context or null
* @return the previous thread local context
*/
protected JexlContext.ThreadLocal putThreadLocal(JexlContext.ThreadLocal tls) {
JexlContext.ThreadLocal local = CONTEXT.get();
CONTEXT.set(tls);
return local;
}
/**
* A soft referenced cache.
* <p>The actual cache is held through a soft reference, allowing it to be GCed under
* memory pressure.</p>
* @param <K> the cache key entry type
* @param <V> the cache key value type
*/
protected class SoftCache<K, V> {
/**
* The cache size.
*/
private final int size;
/**
* The soft reference to the cache map.
*/
private SoftReference<Map<K, V>> 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;
}
/**
* Clears the cache.
*/
void clear() {
ref = null;
}
/**
* Produces the cache entry set.
* @return the cache entry set
*/
Set<Entry<K, V>> entrySet() {
Map<K, V> map = ref != null ? ref.get() : null;
return map != null ? map.entrySet() : Collections.<Entry<K, V>>emptySet();
}
/**
* Gets a value from cache.
* @param key the cache entry key
* @return the cache entry value
*/
V get(K key) {
final Map<K, V> 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<K, V> map = ref != null ? ref.get() : null;
if (map == null) {
map = createCache(size);
ref = new SoftReference<Map<K, V>>(map);
}
map.put(key, script);
}
}
/**
* Creates a cache.
* @param <K> the key type
* @param <V> the value type
* @param cacheSize the cache size, must be &gt; 0
* @return a Map usable as a cache bounded to the given size
*/
protected <K, V> Map<K, V> createCache(final int cacheSize) {
return new java.util.LinkedHashMap<K, V>(cacheSize, LOAD_FACTOR, true) {
/** Serial version UID. */
private static final long serialVersionUID = 1L;
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > cacheSize;
}
};
}
@Override
public void clearCache() {
synchronized (parser) {
if (cache != null) {
cache.clear();
}
}
}
/**
* Creates an interpreter.
* @param context a JexlContext; if null, the empty context is used instead.
* @param frame the interpreter frame
* @return an Interpreter
*/
protected Interpreter createInterpreter(JexlContext context, Scope.Frame frame) {
return new Interpreter(this, context, frame);
}
@Override
public Script createScript(JexlInfo info, String scriptText, String[] names) {
if (scriptText == null) {
throw new NullPointerException("source is null");
}
if (info == null && debug) {
info = createInfo();
}
String source = trimSource(scriptText);
Scope scope = names == null ? null : new Scope(null, names);
ASTJexlScript tree = parse(info, source, scope, false, false);
return new Script(this, source, tree);
}
@Override
public Script createExpression(JexlInfo info, String expression) {
if (expression == null) {
throw new NullPointerException("source is null");
}
if (info == null && debug) {
info = createInfo();
}
String source = trimSource(expression);
ASTJexlScript tree = parse(info, source, null, false, true);
return new Script(this, source, tree);
}
@Override
public Object getProperty(Object bean, String expr) {
return getProperty(null, bean, expr);
}
@Override
public Object getProperty(JexlContext context, Object bean, String expr) {
if (context == null) {
context = EMPTY_CONTEXT;
}
// synthetize expr using register
String src = trimSource(expr);
src = "#0" + (src.charAt(0) == '[' ? "" : ".") + src;
try {
final JexlInfo info = debug ? createInfo() : null;
final Scope scope = new Scope(null, "#0");
final ASTJexlScript script = parse(info, src, scope, true, true);
final JexlNode node = script.jjtGetChild(0);
final Scope.Frame frame = script.createFrame(bean);
final Interpreter interpreter = createInterpreter(context, frame);
return node.jjtAccept(interpreter, null);
} catch (JexlException xjexl) {
if (silent) {
logger.warn(xjexl.getMessage(), xjexl.getCause());
return null;
}
throw xjexl.clean();
}
}
@Override
public void setProperty(Object bean, String expr, Object value) {
setProperty(null, bean, expr, value);
}
@Override
public void setProperty(JexlContext context, Object bean, String expr, Object value) {
if (context == null) {
context = EMPTY_CONTEXT;
}
// synthetize expr using registers
String src = trimSource(expr);
src = "#0" + (src.charAt(0) == '[' ? "" : ".") + src + "=" + "#1";
try {
final JexlInfo info = debug ? createInfo() : null;
final Scope scope = new Scope(null, "#0", "#1");
final ASTJexlScript script = parse(info, src, scope, true, true);
final JexlNode node = script.jjtGetChild(0);
final Scope.Frame frame = script.createFrame(bean, value);
final Interpreter interpreter = createInterpreter(context, frame);
node.jjtAccept(interpreter, null);
} catch (JexlException xjexl) {
if (silent) {
logger.warn(xjexl.getMessage(), xjexl.getCause());
return;
}
throw xjexl.clean();
}
}
@Override
public Object invokeMethod(Object obj, String meth, Object... args) {
JexlException xjexl = null;
Object result = null;
final JexlInfo info = debug ? createInfo() : null;
try {
JexlMethod method = uberspect.getMethod(obj, meth, args);
if (method == null && arithmetic.narrowArguments(args)) {
method = uberspect.getMethod(obj, meth, args);
}
if (method != null) {
result = method.invoke(obj, args);
} else {
xjexl = new JexlException.Method(info, meth, null);
}
} catch (JexlException xany) {
xjexl = xany;
} catch (Exception xany) {
xjexl = new JexlException.Method(info, meth, xany);
}
if (xjexl != null) {
if (silent) {
logger.warn(xjexl.getMessage(), xjexl.getCause());
result = null;
} else {
throw xjexl.clean();
}
}
return result;
}
@Override
public <T> T newInstance(Class<? extends T> clazz, Object... args) {
return clazz.cast(doCreateInstance(clazz, args));
}
@Override
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;
final JexlInfo info = debug ? createInfo() : null;
try {
JexlMethod ctor = uberspect.getConstructor(clazz, args);
if (ctor == null && arithmetic.narrowArguments(args)) {
ctor = uberspect.getConstructor(clazz, args);
}
if (ctor != null) {
result = ctor.invoke(clazz, args);
} else {
xjexl = new JexlException.Method(info, clazz.toString(), null);
}
} catch (JexlException xany) {
xjexl = xany;
} catch (Exception xany) {
xjexl = new JexlException.Method(info, clazz.toString(), xany);
}
if (xjexl != null) {
if (silent) {
logger.warn(xjexl.getMessage(), xjexl.getCause());
return null;
}
throw xjexl.clean();
}
return result;
}
/**
* Gets the list of variables accessed by a script.
* <p>This method will visit all nodes of a script and extract all variables whether they
* are written in 'dot' or 'bracketed' notation. (a.b is equivalent to a['b']).</p>
* @param script the script
* @return the set of variables, each as a list of strings (ant-ish variables use more than 1 string)
* or the empty set if no variables are used
*/
protected Set<List<String>> getVariables(ASTJexlScript script) {
VarCollector collector = new VarCollector();
getVariables(script, script, collector);
return collector.collected();
}
/**
* Utility class to collect variables.
*/
protected static class VarCollector {
/**
* The collected variables represented as a set of list of strings.
*/
private final Set<List<String>> refs = new LinkedHashSet<List<String>>();
/**
* The current variable being collected.
*/
private List<String> ref = new ArrayList<String>();
/**
* The node that started the collect.
*/
private JexlNode root = null;
/**
* Starts/stops a variable collect.
* @param node starts if not null, stop if null
*/
public void collect(JexlNode node) {
if (!ref.isEmpty()) {
refs.add(ref);
ref = new ArrayList<String>();
}
root = node;
}
/**
* @return true if currently collecting a variable, false otherwise
*/
public boolean isCollecting() {
return root instanceof ASTIdentifier;
}
/**
* Adds a 'segment' to the variable being collected.
* @param name the name
*/
public void add(String name) {
ref.add(name);
}
/**
*@return the collected variables
*/
public Set<List<String>> collected() {
return refs;
}
}
/**
* Fills up the list of variables accessed by a node.
* @param script the owning script
* @param node the node
* @param collector the variable collector
*/
protected void getVariables(final ASTJexlScript script, JexlNode node, VarCollector collector) {
if (node instanceof ASTIdentifier) {
JexlNode parent = node.jjtGetParent();
if (parent instanceof ASTMethodNode || parent instanceof ASTFunctionNode) {
// skip identifiers for methods and functions
collector.collect(null);
return;
}
ASTIdentifier identifier = (ASTIdentifier) node;
int symbol = identifier.getSymbol();
// symbols that are hoisted are considered "global" variables
if (symbol >= 0 && script != null && !script.isHoistedSymbol(symbol)) {
collector.collect(null);
} else {
// start collecting from identifier
collector.collect(identifier);
collector.add(identifier.getName());
}
} else if (node instanceof ASTIdentifierAccess) {
JexlNode parent = node.jjtGetParent();
if (parent instanceof ASTMethodNode || parent instanceof ASTFunctionNode) {
// skip identifiers for methods and functions
collector.collect(null);
return;
}
// belt and suspender since an identifier should have been seen first
if (collector.isCollecting()) {
collector.add(((ASTIdentifierAccess) node).getName());
}
} else if (node instanceof ASTArrayAccess) {
int num = node.jjtGetNumChildren();
// collect only if array access is const and follows an identifier
boolean collecting = collector.isCollecting();
for (int i = 0; i < num; ++i) {
JexlNode child = node.jjtGetChild(i);
if (collecting && child.isConstant()) {
String image = child.toString();
collector.add(image);
} else {
collecting = false;
collector.collect(null);
getVariables(script, child, collector);
}
}
} else {
int num = node.jjtGetNumChildren();
for (int i = 0; i < num; ++i) {
getVariables(script, node.jjtGetChild(i), collector);
}
collector.collect(null);
}
}
/**
* Gets the array of parameters from a script.
* @param script the script
* @return the parameters which may be empty (but not null) if no parameters were defined
* @since 3.0
*/
protected String[] getParameters(JexlScript script) {
return script.getParameters();
}
/**
* Gets the array of local variable from a script.
* @param script the script
* @return the local variables array which may be empty (but not null) if no local variables were defined
* @since 3.0
*/
protected String[] getLocalVariables(JexlScript script) {
return script.getLocalVariables();
}
/**
* Parses an expression.
*
* @param info information structure
* @param src the expression to parse
* @param scope the script frame
* @param registers whether the parser should allow the unnamed '#number' syntax for 'registers'
* @param expression whether the parser allows scripts or only expressions
* @return the parsed tree
* @throws JexlException if any error occurred during parsing
*/
protected ASTJexlScript parse(JexlInfo info, String src, Scope scope, boolean registers, boolean expression) {
final boolean cached = src.length() < cacheThreshold && cache != null;
ASTJexlScript script;
synchronized (parser) {
if (cached) {
script = cache.get(src);
if (script != null) {
Scope f = script.getScope();
if ((f == null && scope == null) || (f != null && f.equals(scope))) {
return script;
}
}
}
script = parser.parse(info, src, scope, registers, expression);
if (cached) {
cache.put(src, script);
}
}
return script;
}
/**
* Trims the source from front and ending spaces.
* @param str expression to clean
* @return trimmed expression ending in a semi-colon
*/
protected String trimSource(CharSequence str) {
if (str != null) {
int start = 0;
int end = str.length();
if (end > 0) {
// trim front spaces
while (start < end && Character.isSpaceChar(str.charAt(start))) {
++start;
}
// trim ending spaces
while (end > 0 && Character.isSpaceChar(str.charAt(end - 1))) {
--end;
}
return str.subSequence(start, end).toString();
}
return "";
}
return null;
}
/**
* Gets and/or creates a default template engine.
* @return a template engine
*/
protected TemplateEngine jxlt() {
TemplateEngine e = jxlt;
if (e == null) {
synchronized(this) {
if (jxlt == null) {
e = new TemplateEngine(this, true, 0, '$', '#');
jxlt = e;
}
}
}
return e;
}
}