Engine.java
/*
* 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 static org.apache.commons.jexl3.parser.JexlParser.PRAGMA_IMPORT;
import static org.apache.commons.jexl3.parser.JexlParser.PRAGMA_JEXLNS;
import static org.apache.commons.jexl3.parser.JexlParser.PRAGMA_MODULE;
import static org.apache.commons.jexl3.parser.JexlParser.PRAGMA_OPTIONS;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.IntFunction;
import java.util.function.Predicate;
import org.apache.commons.jexl3.JexlArithmetic;
import org.apache.commons.jexl3.JexlBuilder;
import org.apache.commons.jexl3.JexlCache;
import org.apache.commons.jexl3.JexlContext;
import org.apache.commons.jexl3.JexlEngine;
import org.apache.commons.jexl3.JexlException;
import org.apache.commons.jexl3.JexlFeatures;
import org.apache.commons.jexl3.JexlInfo;
import org.apache.commons.jexl3.JexlOptions;
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.JexlPermissions;
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.ASTNumberLiteral;
import org.apache.commons.jexl3.parser.ASTStringLiteral;
import org.apache.commons.jexl3.parser.JexlNode;
import org.apache.commons.jexl3.parser.Parser;
import org.apache.commons.jexl3.parser.StringProvider;
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. */
static final Uberspect UBERSPECT =
new Uberspect(LogFactory.getLog(JexlEngine.class),
JexlUberspect.JEXL_STRATEGY,
JexlPermissions.parse());
/** Non-instantiable. */
private UberspectHolder() {}
}
/**
* 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<>();
/**
* The current variable being collected.
*/
private List<String> ref = new ArrayList<>();
/**
* The node that started the collect.
*/
private JexlNode root;
/**
* Whether constant array-access is considered equivalent to dot-access;
* if so, > 1 means collect any constant (set,map,...) instead of just
* strings and numbers.
*/
final int mode;
/**
* Constructs a new instance.
* @param constaa whether constant array-access is considered equivalent to dot-access
*/
protected VarCollector(final int constaa) {
mode = constaa;
}
/**
* Adds a 'segment' to the variable being collected.
* @param name the name
*/
public void add(final String name) {
ref.add(name);
}
/**
* Starts/stops a variable collect.
* @param node starts if not null, stop if null
*/
public void collect(final JexlNode node) {
if (!ref.isEmpty()) {
refs.add(ref);
ref = new ArrayList<>();
}
root = node;
}
/**
*@return the collected variables
*/
public Set<List<String>> collected() {
return refs;
}
/**
* @return true if currently collecting a variable, false otherwise
*/
public boolean isCollecting() {
return root instanceof ASTIdentifier;
}
}
/**
* The features allowed for property set/get methods.
*/
protected static final JexlFeatures PROPERTY_FEATURES = new JexlFeatures()
.localVar(false)
.loops(false)
.lambda(false)
.script(false)
.arrayReferenceExpr(false)
.methodCall(false)
.register(true);
/**
* Use {@link Engine#getUberspect(Log, JexlUberspect.ResolverStrategy, JexlPermissions)}.
* @deprecated 3.3
* @param logger the logger
* @param strategy the strategy
* @return an Uberspect instance
*/
@Deprecated
public static Uberspect getUberspect(final Log logger, final JexlUberspect.ResolverStrategy strategy) {
return getUberspect(logger, strategy, 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 and even more so for with a different (restricted) set of permissions.</p>
* @param logger the logger to use for the underlying Uberspect
* @param strategy the property resolver strategy
* @param permissions the introspection permissions
* @return Uberspect the default uberspector instance.
* @since 3.3
*/
public static Uberspect getUberspect(
final Log logger,
final JexlUberspect.ResolverStrategy strategy,
final JexlPermissions permissions) {
if ((logger == null || logger.equals(LogFactory.getLog(JexlEngine.class)))
&& (strategy == null || strategy == JexlUberspect.JEXL_STRATEGY)
&& (permissions == null || permissions == JexlPermissions.UNRESTRICTED)) {
return UberspectHolder.UBERSPECT;
}
return new Uberspect(logger, strategy, permissions);
}
/**
* Solves an optional option.
* @param conf the option as configured, may be null
* @param def the default value if null, shall not be null
* @param <T> the option type
* @return conf or def
*/
private static <T> T option(final T conf, final T def) {
return conf == null ? def : conf;
}
/**
* The Log to which all JexlEngine messages will be logged.
*/
protected final Log logger;
/**
* The JexlUberspect instance.
*/
protected final JexlUberspect uberspect;
/**
* The {@link JexlArithmetic} instance.
*/
protected final JexlArithmetic arithmetic;
/**
* The map of 'prefix:function' to object implementing the namespaces.
*/
protected final Map<String, Object> functions;
/**
* The default class name resolver.
*/
protected final FqcnResolver classNameSolver;
/**
* The maximum stack height.
*/
protected final int stackOverflow;
/**
* Whether this engine considers unknown variables, methods and constructors as errors.
*/
protected final boolean strict;
/**
* Whether this engine considers null in navigation expression as errors.
*/
protected final boolean safe;
/**
* 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 set of default script parsing features.
*/
protected final JexlFeatures scriptFeatures;
/**
* The set of default expression parsing features.
*/
protected final JexlFeatures expressionFeatures;
/**
* The default charset.
*/
protected final Charset charset;
/**
* The atomic parsing flag; true whilst parsing.
*/
protected final AtomicBoolean parsing = new AtomicBoolean();
/**
* The {@link Parser}; when parsing expressions, this engine uses the parser if it
* is not already in use otherwise it will create a new temporary one.
*/
protected final Parser parser = new Parser(new StringProvider(";")); //$NON-NLS-1$
/**
* The expression max length to hit the cache.
*/
protected final int cacheThreshold;
/**
* The expression cache.
*/
protected final JexlCache<Source, ASTJexlScript> cache;
/**
* The default jxlt engine.
*/
protected volatile TemplateEngine jxlt;
/**
* Collect all or only dot references.
*/
protected final int collectMode;
/**
* A cached version of the options.
*/
protected final JexlOptions options;
/**
* The cache factory method.
*/
protected final IntFunction<JexlCache<?, ?>> cacheFactory;
/**
* 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(final JexlBuilder conf) {
// options:
this.options = conf.options().copy();
this.strict = options.isStrict();
this.safe = options.isSafe();
this.silent = options.isSilent();
this.cancellable = option(conf.cancellable(), !silent && strict);
options.setCancellable(cancellable);
this.debug = option(conf.debug(), true);
this.collectMode = conf.collectMode();
this.stackOverflow = conf.stackOverflow() > 0? conf.stackOverflow() : Integer.MAX_VALUE;
// core properties:
final JexlUberspect uber = conf.uberspect() == null
? getUberspect(conf.logger(), conf.strategy(), conf.permissions())
: conf.uberspect();
final ClassLoader loader = conf.loader();
if (loader != null) {
uber.setClassLoader(loader);
}
final JexlSandbox sandbox = conf.sandbox();
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.arithmetic = conf.arithmetic() == null ? new JexlArithmetic(this.strict) : conf.arithmetic();
options.setMathContext(arithmetic.getMathContext());
options.setMathScale(arithmetic.getMathScale());
options.setStrictArithmetic(arithmetic.isStrict());
final Map<String, Object> ns = conf.namespaces();
this.functions = ns == null || ns.isEmpty()? Collections.emptyMap() : ns; // should we make a copy?
this.classNameSolver = new FqcnResolver(uberspect, conf.imports());
// parsing & features:
final JexlFeatures features = conf.features() == null ? DEFAULT_FEATURES : conf.features();
Predicate<String> nsTest = features.namespaceTest();
final Set<String> nsNames = functions.keySet();
if (!nsNames.isEmpty()) {
nsTest = nsTest == JexlFeatures.TEST_STR_FALSE ?nsNames::contains : nsTest.or(nsNames::contains);
}
this.expressionFeatures = new JexlFeatures(features).script(false).namespaceTest(nsTest);
this.scriptFeatures = new JexlFeatures(features).script(true).namespaceTest(nsTest);
this.charset = conf.charset();
// caching:
final IntFunction<JexlCache<?, ?>> factory = conf.cacheFactory();
this.cacheFactory = factory == null ? SoftCache::new : factory;
this.cache = (JexlCache<Source, ASTJexlScript>) (conf.cache() > 0 ? factory.apply(conf.cache()) : null);
this.cacheThreshold = conf.cacheThreshold();
if (uberspect == null) {
throw new IllegalArgumentException("uberspect can not be null");
}
}
@Override
public void clearCache() {
if (cache != null) {
cache.clear();
}
}
@Override
public Script createExpression(final JexlInfo info, final String expression) {
return createScript(expressionFeatures, info, expression);
}
/**
* Creates an interpreter.
* @param context a JexlContext; if null, the empty context is used instead.
* @param frame the interpreter frame
* @param opts the evaluation options
* @return an Interpreter
*/
protected Interpreter createInterpreter(final JexlContext context, final Frame frame, final JexlOptions opts) {
return new Interpreter(this, opts, context, frame);
}
@Override
public TemplateEngine createJxltEngine(final boolean noScript, final int cacheSize, final char immediate, final char deferred) {
return new TemplateEngine(this, noScript, cacheSize, immediate, deferred);
}
@Override
public Script createScript(final JexlFeatures features, final JexlInfo info, final String scriptText, final String... names) {
if (scriptText == null) {
throw new NullPointerException("source is null");
}
final String source = trimSource(scriptText);
final Scope scope = names == null || names.length == 0? null : new Scope(null, names);
final JexlFeatures ftrs = features == null ? scriptFeatures : features;
final ASTJexlScript tree = parse(info, ftrs, source, scope);
return new Script(this, source, tree);
}
/**
* Creates a template interpreter.
* @param args the template interpreter arguments
*/
protected Interpreter createTemplateInterpreter(final TemplateInterpreter.Arguments args) {
return new TemplateInterpreter(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(final Object clazz, final 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(), args);
}
} catch (final JexlException xany) {
xjexl = xany;
} catch (final Exception xany) {
xjexl = new JexlException.Method(info, clazz.toString(), args, xany);
}
if (xjexl != null) {
if (silent) {
if (logger.isWarnEnabled()) {
logger.warn(xjexl.getMessage(), xjexl.getCause());
}
return null;
}
throw xjexl.clean();
}
return result;
}
/**
* Compute a script options for evaluation.
* <p>This calls processPragma(...).
* @param script the script
* @param context the context
* @return the options
*/
protected JexlOptions evalOptions(final ASTJexlScript script, final JexlContext context) {
final JexlOptions opts = evalOptions(context);
if (opts != options) {
// when feature lexical, try hard to run lexical
if (scriptFeatures.isLexical()) {
opts.setLexical(true);
}
if (scriptFeatures.isLexicalShade()) {
opts.setLexicalShade(true);
}
if (scriptFeatures.supportsConstCapture()) {
opts.setConstCapture(true);
}
}
if (script != null) {
// process script pragmas if any
processPragmas(script, context, opts);
}
return opts;
}
/**
* Extracts the engine evaluation options from context if available, the engine
* options otherwise.
* <p>If the context is an options handle and the handled options shared instance flag
* is false, this method creates a copy of the options making them immutable during execution.
* @param context the context
* @return the options if any
*/
protected JexlOptions evalOptions(final JexlContext context) {
// Make a copy of the handled options if any
if (context instanceof JexlContext.OptionsHandle) {
final JexlOptions jexlo = ((JexlContext.OptionsHandle) context).getEngineOptions();
if (jexlo != null) {
return jexlo.isSharedInstance()? jexlo : jexlo.copy();
}
} else if (context instanceof JexlEngine.Options) {
return evalOptions((JexlEngine.Options) context);
}
return options;
}
/**
* Obsolete version of options evaluation.
* @param opts the obsolete instance of options
* @return the newer class of options
*/
private JexlOptions evalOptions(final JexlEngine.Options opts) {
// This condition and block for compatibility between 3.1 and 3.2
final JexlOptions jexlo = options.copy();
final JexlEngine jexl = this;
jexlo.setCancellable(option(opts.isCancellable(), jexl.isCancellable()));
jexlo.setSilent(option(opts.isSilent(), jexl.isSilent()));
jexlo.setStrict(option(opts.isStrict(), jexl.isStrict()));
final JexlArithmetic jexla = jexl.getArithmetic();
jexlo.setStrictArithmetic(option(opts.isStrictArithmetic(), jexla.isStrict()));
jexlo.setMathContext(opts.getArithmeticMathContext());
jexlo.setMathScale(opts.getArithmeticMathScale());
return jexlo;
}
@Override
public JexlArithmetic getArithmetic() {
return arithmetic;
}
@Override
public Charset getCharset() {
return charset;
}
/**
* 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(final JexlScript script) {
return script.getLocalVariables();
}
/**
* Solves a namespace using this engine map of functions.
* @param name the namespoce name
* @return the object associated
*/
final Object getNamespace(final String name) {
return functions.get(name);
}
/**
* 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(final JexlScript script) {
return script.getParameters();
}
@Override
public Object getProperty(final JexlContext context, final Object bean, final String expr) {
// synthesize expr using register
String src = trimSource(expr);
src = "#0" + (src.charAt(0) == '[' ? "" : ".") + src;
try {
final Scope scope = new Scope(null, "#0");
final ASTJexlScript script = parse(null, PROPERTY_FEATURES, src, scope);
final JexlNode node = script.jjtGetChild(0);
final Frame frame = script.createFrame(bean);
final Interpreter interpreter = createInterpreter(context == null ? EMPTY_CONTEXT : context, frame, options);
return interpreter.visitLexicalNode(node, null);
} catch (final JexlException xjexl) {
if (silent) {
if (logger.isWarnEnabled()) {
logger.warn(xjexl.getMessage(), xjexl.getCause());
}
return null;
}
throw xjexl.clean();
}
}
@Override
public Object getProperty(final Object bean, final String expr) {
return getProperty(null, bean, expr);
}
@Override
public JexlUberspect getUberspect() {
return uberspect;
}
/**
* 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(final ASTJexlScript script) {
final VarCollector collector = varCollector();
getVariables(script, script, collector);
return collector.collected();
}
/**
* 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, final JexlNode node, final VarCollector collector) {
if (node instanceof ASTIdentifier) {
final JexlNode parent = node.jjtGetParent();
if (parent instanceof ASTMethodNode || parent instanceof ASTFunctionNode) {
// skip identifiers for methods and functions
collector.collect(null);
return;
}
final ASTIdentifier identifier = (ASTIdentifier) node;
final int symbol = identifier.getSymbol();
// symbols that are captured are considered "global" variables
if (symbol >= 0 && script != null && !script.isCapturedSymbol(symbol)) {
collector.collect(null);
} else {
// start collecting from identifier
collector.collect(identifier);
collector.add(identifier.getName());
}
} else if (node instanceof ASTIdentifierAccess) {
final 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 && collector.mode > 0) {
final 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) {
final JexlNode child = node.jjtGetChild(i);
if (collecting && child.isConstant()) {
// collect all constants or only string and number literals
final boolean collect = collector.mode > 1
|| child instanceof ASTStringLiteral || child instanceof ASTNumberLiteral;
if (collect) {
final String image = child.toString();
collector.add(image);
}
} else {
collecting = false;
collector.collect(null);
getVariables(script, child, collector);
collector.collect(null);
}
}
} else {
final int num = node.jjtGetNumChildren();
for (int i = 0; i < num; ++i) {
getVariables(script, node.jjtGetChild(i), collector);
}
collector.collect(null);
}
}
@Override
public Object invokeMethod(final Object obj, final String meth, final 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, args);
}
} catch (final JexlException xany) {
xjexl = xany;
} catch (final Exception xany) {
xjexl = new JexlException.Method(info, meth, args, xany);
}
if (xjexl != null) {
if (!silent) {
throw xjexl.clean();
}
if (logger.isWarnEnabled()) {
logger.warn(xjexl.getMessage(), xjexl.getCause());
}
}
return result;
}
@Override
public boolean isCancellable() {
return this.cancellable;
}
@Override
public boolean isDebug() {
return this.debug;
}
@Override
public boolean isSilent() {
return this.silent;
}
@Override
public boolean isStrict() {
return this.strict;
}
/**
* Gets and/or creates a default template engine.
* @return a template engine
*/
protected TemplateEngine jxlt() {
TemplateEngine e = jxlt;
if (e == null) {
synchronized(this) {
e = jxlt;
if (e == null) {
e = new TemplateEngine(this, true, 0, '$', '#');
jxlt = e;
}
}
}
return e;
}
@Override
public <T> T newInstance(final Class<? extends T> clazz, final Object... args) {
return clazz.cast(doCreateInstance(clazz, args));
}
@Override
public Object newInstance(final String clazz, final Object... args) {
return doCreateInstance(clazz, args);
}
/**
* Sets options from this engine options.
* @param opts the options to set
* @return the options
*/
public JexlOptions optionsSet(final JexlOptions opts) {
if (opts != null) {
opts.set(options);
}
return opts;
}
/**
* Parses an expression.
*
* @param info information structure
* @param expr whether we parse an expression or a feature
* @param src the expression to parse
* @param scope the script frame
* @return the parsed tree
* @throws JexlException if any error occurred during parsing
*/
protected ASTJexlScript parse(final JexlInfo info, final boolean expr, final String src, final Scope scope) {
return parse(info, expr? this.expressionFeatures : this.scriptFeatures, src, scope);
}
/**
* Parses an expression.
*
* @param info information structure
* @param parsingf the set of parsing features
* @param src the expression to parse
* @param scope the script frame
* @return the parsed tree
* @throws JexlException if any error occurred during parsing
*/
protected ASTJexlScript parse(final JexlInfo info, final JexlFeatures parsingf, final String src, final Scope scope) {
final boolean cached = src.length() < cacheThreshold && cache != null;
final JexlFeatures features = parsingf != null ? parsingf : DEFAULT_FEATURES;
final Source source = cached? new Source(features, src) : null;
ASTJexlScript script;
if (source != null) {
script = cache.get(source);
if (script != null) {
final Scope f = script.getScope();
if (f == null && scope == null || f != null && f.equals(scope)) {
return script;
}
}
}
final JexlInfo ninfo = info == null && debug ? createInfo() : info;
// if parser not in use...
if (parsing.compareAndSet(false, true)) {
try {
// lets parse
script = parser.parse(ninfo, features, src, scope);
} finally {
// no longer in use
parsing.set(false);
}
} else {
// ...otherwise parser was in use, create a new temporary one
final Parser lparser = new Parser(new StringProvider(";"));
script = lparser.parse(ninfo, features, src, scope);
}
if (source != null) {
cache.put(source, script);
}
return script;
}
/**
* Processes jexl.module.ns pragma.
*
* <p>If the value is empty, the namespace will be cleared which may be useful to debug and force unload
* the object bound to the namespace.</p>
* @param ns the namespace map
* @param key the key the namespace
* @param value the value, ie the expression to evaluate and its result bound to the namespace
* @param info the expression info
* @param context the value-as-expression evaluation context
*/
private void processPragmaModule(final Map<String, Object> ns, final String key, final Object value, final JexlInfo info,
final JexlContext context) {
// jexl.module.***
final String module = key.substring(PRAGMA_MODULE.length());
if (module.isEmpty()) {
if (logger.isWarnEnabled()) {
logger.warn(module + ": invalid module declaration");
}
} else {
withValueSet(value, o -> {
if (!(o instanceof CharSequence)) {
if (logger.isWarnEnabled()) {
logger.warn(module + ": unable to define module from " + value);
}
} else {
final String moduleSrc = o.toString();
final Object functor;
if (context instanceof JexlContext.ModuleProcessor) {
final JexlContext.ModuleProcessor processor = (JexlContext.ModuleProcessor) context;
functor = processor.processModule(this, info, module, moduleSrc);
} else {
final Object moduleObject = createExpression(info, moduleSrc).evaluate(context);
functor = moduleObject instanceof Script ? ((Script) moduleObject).execute(context) : moduleObject;
}
if (functor != null) {
ns.put(module, functor);
} else {
ns.remove(module);
}
}
});
}
}
/**
* Processes jexl.namespace.ns pragma.
* @param ns the namespace map
* @param key the key
* @param value the value, ie the class
*/
private void processPragmaNamespace(final Map<String, Object> ns, final String key, final Object value) {
if (value instanceof String) {
// jexl.namespace.***
final String namespaceName = key.substring(PRAGMA_JEXLNS.length());
if (!namespaceName.isEmpty()) {
final String nsclass = value.toString();
final Class<?> clazz = uberspect.getClassByName(nsclass);
if (clazz == null) {
if (logger.isWarnEnabled()) {
logger.warn(key + ": unable to find class " + nsclass);
}
} else {
ns.put(namespaceName, clazz);
}
}
} else if (logger.isWarnEnabled()) {
logger.warn(key + ": ambiguous declaration " + value);
}
}
/**
* Processes a script pragmas.
* <p>Only called from options(...)
* @param script the script
* @param context the context
* @param opts the options
*/
protected void processPragmas(final ASTJexlScript script, final JexlContext context, final JexlOptions opts) {
final Map<String, Object> pragmas = script.getPragmas();
if (pragmas != null && !pragmas.isEmpty()) {
final JexlContext.PragmaProcessor processor =
context instanceof JexlContext.PragmaProcessor
? (JexlContext.PragmaProcessor) context
: null;
Map<String, Object> ns = null;
for (final Map.Entry<String, Object> pragma : pragmas.entrySet()) {
final String key = pragma.getKey();
final Object value = pragma.getValue();
if (PRAGMA_OPTIONS.equals(key)) {
if (value instanceof String) {
// jexl.options
final String[] vs = value.toString().split(" ");
opts.setFlags(vs);
}
} else if (PRAGMA_IMPORT.equals(key)) {
// jexl.import, may use a set
final Set<String> is = new LinkedHashSet<>();
withValueSet(value, o -> {
if (o instanceof String) {
is.add(o.toString());
}
});
if (!is.isEmpty()) {
opts.setImports(is);
}
} else if (key.startsWith(PRAGMA_JEXLNS)) {
if (ns == null) {
ns = new LinkedHashMap<>();
}
processPragmaNamespace(ns, key, value);
if (!ns.isEmpty()) {
opts.setNamespaces(ns);
}
} else if (key.startsWith(PRAGMA_MODULE)) {
if (ns == null) {
ns = new LinkedHashMap<>();
}
processPragmaModule(ns, key, value, script.jexlInfo(), context);
if (!ns.isEmpty()) {
opts.setNamespaces(ns);
}
}
// user-defined processor may alter options
if (processor != null) {
processor.processPragma(opts, key, value);
}
}
}
}
/**
* Swaps the current thread local engine.
* @param jexl the engine or null
* @return the previous thread local engine
*/
protected JexlEngine putThreadEngine(final JexlEngine jexl) {
final JexlEngine pjexl = ENGINE.get();
ENGINE.set(jexl);
return pjexl;
}
/**
* Swaps the current thread local context.
* @param tls the context or null
* @return the previous thread local context
*/
protected JexlContext.ThreadLocal putThreadLocal(final JexlContext.ThreadLocal tls) {
final JexlContext.ThreadLocal local = CONTEXT.get();
CONTEXT.set(tls);
return local;
}
@Override
public void setClassLoader(final ClassLoader loader) {
jxlt = null;
uberspect.setClassLoader(loader);
if (functions != null) {
final Iterable<String> names = new ArrayList<>(functions.keySet());
for(final String name : names) {
final Object functor = functions.get(name);
if (functor instanceof Class<?>) {
final Class<?> fclass = (Class<?>) functor;
try {
final Class<?> nclass = loader.loadClass(fclass.getName());
if (nclass != fclass) {
functions.put(name, nclass);
}
} catch (final ClassNotFoundException xany) {
functions.put(name, fclass.getName());
}
}
}
}
if (cache != null) {
cache.clear();
}
}
@Override
public void setProperty(final JexlContext context, final Object bean, final String expr, final Object value) {
// synthesize expr using register
String src = trimSource(expr);
src = "#0" + (src.charAt(0) == '[' ? "" : ".") + src + "=" + "#1";
try {
final Scope scope = new Scope(null, "#0", "#1");
final ASTJexlScript script = parse(null, PROPERTY_FEATURES, src, scope);
final JexlNode node = script.jjtGetChild(0);
final Frame frame = script.createFrame(bean, value);
final Interpreter interpreter = createInterpreter(context != null ? context : EMPTY_CONTEXT, frame, options);
interpreter.visitLexicalNode(node, null);
} catch (final JexlException xjexl) {
if (silent) {
if (logger.isWarnEnabled()) {
logger.warn(xjexl.getMessage(), xjexl.getCause());
}
return;
}
throw xjexl.clean();
}
}
@Override
public void setProperty(final Object bean, final String expr, final Object value) {
setProperty(null, bean, expr, value);
}
/**
* Trims the source from front and ending spaces.
* @param str expression to clean
* @return trimmed expression ending in a semicolon
*/
protected String trimSource(final 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; end is > 0 since start >= 0
while (end > start && Character.isSpaceChar(str.charAt(end - 1))) {
--end;
}
return str.subSequence(start, end).toString();
}
return "";
}
return null;
}
/**
* Creates a collector instance.
* @return a collector instance
*/
protected VarCollector varCollector() {
return new VarCollector(this.collectMode);
}
/**
* Utility to deal with single value or set of values.
* @param value the value or the set
* @param consumer the consumer of values
*/
private void withValueSet(final Object value, final Consumer<Object> consumer) {
final Set<?> values = value instanceof Set<?>
? (Set<?>) value
: Collections.singleton(value);
for (final Object o : values) {
consumer.accept(o);
}
}
}