/* * Copyright 2016 Nabarun Mondal * Licensed 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 com.noga.njexl.lang; import java.io.*; import java.net.URL; import java.net.URLConnection; import java.lang.ref.SoftReference; import java.util.*; import java.util.Map.Entry; import com.noga.njexl.lang.internal.logging.Log; import com.noga.njexl.lang.internal.logging.LogFactory; import com.noga.njexl.lang.introspection.Uberspect; import com.noga.njexl.lang.introspection.UberspectImpl; import com.noga.njexl.lang.parser.*; import com.noga.njexl.lang.introspection.JexlMethod; /** Creates and evaluates Expression and Script objects. Determines the behavior of Expressions and Scripts during their evaluation with respect to: <ul> <li>Introspection, see {@link Uberspect}</li> <li>Arithmetic and comparison, see {@link JexlArithmetic}</li> <li>Error reporting</li> <li>Logging</li> </ul> The <code>setSilent</code> and <code>setLenient</code> methods allow to fine-tune an engine instance behavior according to various error control needs. The lenient/strict flag tells the engine when and if null as operand is considered an error, the silent/verbose flag tells the engine what to do with the error (log as warning or throw exception). <ul> <li>When "silent" & "lenient": 0 and 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. </li> <li>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. </li> <li>When "verbose" & "lenient": The error control grain is roughly on par with JEXL 1.0 </li> <li>When "verbose" & "strict": The finest error control grain is obtained; it is the closest to Java code - still augmented by "script" capabilities regarding automated conversions and type matching. </li> </ul> Note that methods that evaluate expressions may throw <em>unchecked</em> exceptions; The {@link JexlException} are thrown in "non-silent" mode but since these are RuntimeException, user-code <em>should</em> catch them wherever most appropriate. * * @since 2.0 */ public class JexlEngine { private static final Log LOGGER = LogFactory.getLog(JexlEngine.class); public static final String LINE = System.lineSeparator(); /** * 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."); } /** {@inheritDoc} */ public void remove(String name) { throw new UnsupportedOperationException("Not supported in void context."); } /** {@inheritDoc} */ @Override public JexlContext copy() { return this; } /** {@inheritDoc} */ @Override public void clear() { /* it is, after all, an epty context !*/ } }; /** * 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 UberspectImpl(LogFactory.getLog(JexlEngine.class)); /** * Non-instantiable. */ private UberspectHolder() { } } boolean shareImports = false ; /** * Gets the import status of the engine * @return true if import would be shared, false if otherwise */ public boolean shareImports() { return shareImports; } /** * <pre> * If one reuse the JexlEngine to import multiple jexl scripts, * then there is possibility of import collision. * Example, both scripts might want to import System.out as out. * This is disaster, and thus is not allowed. * Thus, the previous scripts imports are never propagated to the * newer scripts. * However, in command line interpreter mode, one is forced to do otherwise. * In that case, we must share the imports, otherwise, one can not * use imports thus made in the previous step. * Note that the context is always shared, as it should be, * immaterial of the import mode sharing. * The only use as of now of this is the Main() function. * </pre> * @param shareImports to share imports or not to share the imports */ public void shareImports(boolean shareImports) { this.shareImports = shareImports; } HashMap<String, Script> imports; /** * 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) on errors. Default is false. */ // TODO could this be private? protected volatile boolean silent = false; /** * Whether error messages will carry debugging information. */ // TODO could this be private? protected volatile boolean debug = true; /** * The map of 'prefix:function' to object implementing the functions. */ // TODO this could probably be private; is it threadsafe? protected Map<String, Object> functions = Collections.emptyMap(); /** * The expression cache. */ // TODO is this thread-safe? Could it be made private? protected SoftCache<String, ASTJexlScript> 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<String, Object> 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; } imports = new HashMap<>(); } /** * 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 * @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; } /** * Gets this engine underlying arithmetic. * * @return the arithmetic * @since 2.1 */ public JexlArithmetic getArithmetic() { return arithmetic; } /** * Sets whether this engine reports debugging information when error occurs. * <p>This method is <em>not</em> thread safe; it should be called as an optional step of the JexlEngine * initialization code before expression creation & evaluation.</p> * * @param flag true implies debug is on, false implies debug is off. * @see JexlEngine#setSilent * @see JexlEngine#setLenient */ 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. * <p>This method is <em>not</em> thread safe; it should be called as an optional step of the JexlEngine * initialization code before expression creation & evaluation.</p> * * @param flag true means no JexlException will occur, false allows them * @see JexlEngine#setDebug * @see JexlEngine#setLenient */ 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 considers unknown variables, methods and constructors as errors or evaluates them * as null or zero. * <p>This method is <em>not</em> thread safe; it should be called as an optional step of the JexlEngine * initialization code before expression creation & evaluation.</p> * <p>As of 2.1, you can use a JexlThreadedArithmetic instance to allow the JexlArithmetic * leniency behavior to be independently specified per thread, whilst still using a single engine.</p> * * @param flag true means no JexlException will occur, false allows them * @see JexlEngine#setSilent * @see JexlEngine#setDebug */ @SuppressWarnings("deprecation") public void setLenient(boolean flag) { if (arithmetic instanceof JexlThreadedArithmetic) { JexlThreadedArithmetic.setLenient(Boolean.valueOf(flag)); } else { this.arithmetic.setLenient(flag); } } /** * Checks whether this engine considers unknown variables, methods and constructors as errors. * * @return true if lenient, false if strict */ public boolean isLenient() { return arithmetic.isLenient(); } /** * Should we throw error in case of undefined variables? */ protected boolean errorOnUndefinedVariable = true; public boolean isErrorOnUndefinedVariable() { return errorOnUndefinedVariable; } public void errorOnUndefinedVariable(boolean throwError) { errorOnUndefinedVariable = throwError; } /** * Sets whether this engine behaves in strict or lenient mode. * Equivalent to setLenient(!flag). * <p>This method is <em>not</em> thread safe; it should be called as an optional step of the JexlEngine * initialization code before expression creation & evaluation.</p> * * @param flag true for strict, false for lenient * @since 2.1 */ public final void setStrict(boolean flag) { setLenient(!flag); } /** * Checks whether this engine behaves in strict or lenient mode. * Equivalent to !isLenient(). * * @return true for strict, false for lenient * @since 2.1 */ public final boolean isStrict() { return !isLenient(); } /** * Sets the class loader used to discover classes in 'new' expressions. * <p>This method should be called as an optional step of the JexlEngine * initialization code before expression creation & evaluation.</p> * * @param loader the class loader to use */ public void setClassLoader(ClassLoader loader) { uberspect.setClassLoader(loader); } /** * Sets a cache for expressions of the defined size. * <p>The cache will contain at most <code>size</code> expressions. Note that * all JEXL caches are held through SoftReferences and may be garbage-collected.</p> * * @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<String, ASTJexlScript>(size); } } } /** * Sets the map of function namespaces. * <p> * This method is <em>not</em> thread safe; it should be called as an optional step of the JexlEngine * initialization code before expression creation & evaluation. * </p> * <p> * 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. * </p> * <p> * 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. * </p> * <p> * 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(...) ) * </p> * <p>Note that the JexlContext is also used to try to solve top-level functions. This allows ObjectContext * derived instances to call methods on the wrapped object.</p> * * @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<String, Object> funcs) { functions = funcs != null ? funcs : Collections.<String, Object>emptyMap(); } /** * Retrieves the map of function namespaces. * * @return the map passed in setFunctions or the empty map if the * original was null. */ public Map<String, Object> 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 * @param info An info structure to carry debugging information if needed * @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 or a reference. */ public Expression createExpression(String expression, JexlInfo info) { // Parse the expression ASTJexlScript tree = parse(expression, info, null); 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, null); } public Script createCopyScript(String scriptText, Script parent) { Script child = createScript(scriptText, null, null); if (parent != null) { child.imports().putAll(parent.imports()); child.methods().putAll(parent.methods()); } return child; } /** * 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. * @deprecated Use {@link #createScript(String, JexlInfo, String[])} */ @Deprecated 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); } /** * 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 names the script parameter names * @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, String... names) { return createScript(scriptText, null, names); } public Script importScript(String from) throws Exception { return importScript(from, Script.DEFAULT_IMPORT_NAME); } public Script importScript(String from, String as) throws Exception { return importScript(from, as, null); } File tryFindFile(String from) throws Exception{ File f = new File(from); String d = f.getParent(); if ( d == null ) { d = System.getProperty("user.dir"); } File dir = new File(d); File[] files = dir.listFiles(); String sep = "/" ; if ( from.contains("\\")){ sep = "\\" ; } String[] paths = from.split(sep); String name = paths[ paths.length-1 ]; for ( File file : files ){ String fileName = file.getName() ; boolean b = fileName.startsWith(name) && Script.DEFAULT_NAME_MATCH.matcher(fileName).matches(); if ( b ) return file; } String msg = String.format("No jexl file named '%s' found in dir : %s", f.getName(), d ); throw new FileNotFoundException(msg); } /** * Import a script from a location * * @param from the location where it needs to be imported * @param as the directive as it needs to be imported * @param base the parent script from where import should happen * @return script * @throws Exception if fails, throws exception */ public Script importScript(String from, String as, Script base) throws Exception { String scriptText; if (from.startsWith(Script.RELATIVE)) { if ( base == null ) { from = System.getProperty("user.dir") + from.substring(1); }else{ from = base.location() + from.substring(1); } } File f = new File(from); String n = f.getName(); if ( !n.contains(".")){ // I do not have extension : f = tryFindFile(from); } BufferedReader reader = new BufferedReader(new FileReader(f)); scriptText = readerToString(reader); // remove the first #! if any ? if ( scriptText.startsWith("#!")){ int lineIndex = scriptText.indexOf(LINE); scriptText = scriptText.substring(lineIndex); } // name mangling for linking scriptText = scriptText.replaceAll("\b"+Script.SELF +"\b" + ":", as + ":"); // now create script // Parse the expression String path = f.getCanonicalPath(); ASTJexlScript tree = parse(scriptText, createInfo(path, 0, 0), new Scope(null)); Script script = new ExpressionImpl(path, as, this, scriptText, tree); LOGGER.trace( String.format("Script imported : %s@%s\n", as, path)); imports.put(as, script); return script; } /** * Creates a Script from a String containing valid JEXL syntax. * This method parses the script which validates the syntax. * It uses an array of parameter names that will be resolved during parsing; * a corresponding array of arguments containing values should be used during evaluation. * * @param scriptText A String containing valid JEXL syntax * @param info An info structure to carry debugging information if needed * @param names the script parameter names * @return A {@link Script} which can be executed using a {@link JexlContext}. * @throws JexlException if there is a problem parsing the script. * @since 2.1 */ public Script createScript(String scriptText, JexlInfo info, String[] names) { if (scriptText == null) { throw new NullPointerException("scriptText is null"); } // Parse the expression ASTJexlScript tree = parse(scriptText, info, new Scope(names)); 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, null); } /** * 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, null); } /** * Accesses properties of a bean using an expression. * <p> * jexl.get(myobject, "foo.bar"); should equate to * myobject.getFoo().getBar(); (or myobject.getFoo().get("bar")) * </p> * <p> * If the JEXL engine is silent, errors will be logged through its logger as warning. * </p> * * @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. * <p> * If the JEXL engine is silent, errors will be logged through its logger as warning. * </p> * * @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; } // synthetize expr using register expr = "#0" + (expr.charAt(0) == '[' ? "" : ".") + expr + ";"; try { parser.ALLOW_REGISTERS = true; Scope frame = new Scope("#0"); ASTJexlScript script = parse(expr, null, frame); JexlNode node = script.jjtGetChild(0); Interpreter interpreter = createInterpreter(context); // set frame interpreter.setFrame(script.createFrame(bean)); return node.jjtAccept(interpreter, null); } catch (JexlException xjexl) { if (silent) { logger.warn(xjexl.getMessage(), xjexl.getCause()); return null; } throw xjexl; } finally { parser.ALLOW_REGISTERS = false; } } /** * Assign properties of a bean using an expression. * <p> * jexl.set(myobject, "foo.bar", 10); should equate to * myobject.getFoo().setBar(10); (or myobject.getFoo().put("bar", 10) ) * </p> * <p> * If the JEXL engine is silent, errors will be logged through its logger as warning. * </p> * * @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. * <p> * If the JEXL engine is silent, errors will be logged through its logger as warning. * </p> * * @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; } // synthetize expr using registers expr = "#0" + (expr.charAt(0) == '[' ? "" : ".") + expr + "=" + "#1" + ";"; try { parser.ALLOW_REGISTERS = true; Scope frame = new Scope("#0", "#1"); ASTJexlScript script = parse(expr, null, frame); JexlNode node = script.jjtGetChild(0); Interpreter interpreter = createInterpreter(context); // set the registers interpreter.setFrame(script.createFrame(bean, value)); node.jjtAccept(interpreter, null); } catch (JexlException xjexl) { if (silent) { logger.warn(xjexl.getMessage(), xjexl.getCause()); return; } throw xjexl; } finally { parser.ALLOW_REGISTERS = false; } } /** * 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 <T> 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> 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 { JexlMethod ctor = uberspect.getConstructorMethod(clazz, args, info); if (ctor == null && arithmetic.narrowArguments(args)) { ctor = uberspect.getConstructorMethod(clazz, args, info); } if (ctor != null) { result = ctor.invoke(clazz, 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) { return createInterpreter(context, isStrict(), isSilent()); } /** * Creates an interpreter. * * @param context a JexlContext; if null, the EMPTY_CONTEXT is used instead. * @param strictFlag whether the interpreter runs in strict mode * @param silentFlag whether the interpreter runs in silent mode * @return an Interpreter * @since 2.1 */ protected Interpreter createInterpreter(JexlContext context, boolean strictFlag, boolean silentFlag) { Interpreter interpreter = new Interpreter(this, context == null ? EMPTY_CONTEXT : context, strictFlag, silentFlag); interpreter.errorOnUndefinedVariable = this.errorOnUndefinedVariable; return interpreter; } /** * A soft reference on cache. * <p>The 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 > 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; } }; } /** * Clears the expression cache. * * @since 2.1 */ public void clearCache() { synchronized (parser) { cache.clear(); } } /** * 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 * @since 2.1 */ public Set<List<String>> getVariables(Script script) { if (script instanceof ExpressionImpl) { Set<List<String>> refs = new LinkedHashSet<List<String>>(); getVariables(((ExpressionImpl) script).script, refs, null); return refs; } else { return Collections.<List<String>>emptySet(); } } /** * Fills up the list of variables accessed by a node. * * @param node the node * @param refs the set of variable being filled * @param ref the current variable being filled * @since 2.1 */ protected void getVariables(JexlNode node, Set<List<String>> refs, List<String> ref) { boolean array = node instanceof ASTArrayAccess; boolean reference = node instanceof ASTReference; int num = node.jjtGetNumChildren(); if (array || reference) { List<String> var = ref != null ? ref : new ArrayList<String>(); boolean varf = true; for (int i = 0; i < num; ++i) { JexlNode child = node.jjtGetChild(i); if (array) { if (child instanceof ASTReference && child.jjtGetNumChildren() == 1) { JexlNode desc = child.jjtGetChild(0); if (varf && desc.isConstant()) { String image = desc.image; if (image == null) { var.add(new Debugger().data(desc)); } else { var.add(image); } } else if (desc instanceof ASTIdentifier) { if (((ASTIdentifier) desc).getRegister() < 0) { List<String> di = new ArrayList<String>(1); di.add(desc.image); refs.add(di); } var = new ArrayList<String>(); varf = false; } continue; } else if (child instanceof ASTIdentifier) { if (i == 0 && (((ASTIdentifier) child).getRegister() < 0)) { var.add(child.image); } continue; } } else {//if (reference) { if (child instanceof ASTIdentifier) { if (((ASTIdentifier) child).getRegister() < 0) { var.add(child.image); } continue; } } getVariables(child, refs, var); } if (!var.isEmpty() && var != ref) { refs.add(var); } } else { for (int i = 0; i < num; ++i) { getVariables(node.jjtGetChild(i), refs, 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 2.1 */ protected String[] getParameters(Script script) { if (script instanceof ExpressionImpl) { return ((ExpressionImpl) script).getParameters(); } else { return new String[0]; } } /** * 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 2.1 */ protected String[] getLocalVariables(Script script) { if (script instanceof ExpressionImpl) { return ((ExpressionImpl) script).getLocalVariables(); } else { return new String[0]; } } /** * A script scope, stores the declaration of parameters and local variables. * * @since 2.1 */ public static final class Scope { /** * The number of parameters. */ private final int parms; /** * The map of named registers aka script parameters. * Each parameter is associated to a register and is materialized as an offset in the registers array used * during evaluation. */ private Map<String, Integer> namedRegisters = null; /** * Creates a new scope with a list of parameters. * * @param parameters the list of parameters */ public Scope(String... parameters) { if (parameters != null) { parms = parameters.length; namedRegisters = new LinkedHashMap<String, Integer>(); for (int p = 0; p < parms; ++p) { namedRegisters.put(parameters[p], Integer.valueOf(p)); } } else { parms = 0; } } @Override public int hashCode() { return namedRegisters == null ? 0 : parms ^ namedRegisters.hashCode(); } @Override public boolean equals(Object o) { return o instanceof Scope && equals((Scope) o); } /** * Whether this frame is equal to another. * * @param frame the frame to compare to * @return true if equal, false otherwise */ public boolean equals(Scope frame) { if (this == frame) { return true; } else if (frame == null || parms != frame.parms) { return false; } else if (namedRegisters == null) { return frame.namedRegisters == null; } else { return namedRegisters.equals(frame.namedRegisters); } } /** * Checks whether an identifier is a local variable or argument, ie stored in a register. * * @param name the register name * @return the register index */ public Integer getRegister(String name) { return namedRegisters != null ? namedRegisters.get(name) : null; } /** * Declares a local variable. * <p> * This method creates an new entry in the named register map. * </p> * * @param name the variable name * @return the register index storing this variable */ public Integer declareVariable(String name) { if (namedRegisters == null) { namedRegisters = new LinkedHashMap<String, Integer>(); } Integer register = namedRegisters.get(name); if (register == null) { register = Integer.valueOf(namedRegisters.size()); namedRegisters.put(name, register); } return register; } /** * Creates a frame by copying values up to the number of parameters. * * @param values the argument values * @return the arguments array */ public Frame createFrame(Object... values) { if (namedRegisters != null) { Object[] arguments = new Object[namedRegisters.size()]; if (values != null) { System.arraycopy(values, 0, arguments, 0, Math.min(parms, values.length)); } return new Frame(arguments, namedRegisters.keySet().toArray(new String[0])); } else { return null; } } /** * Gets the (maximum) number of arguments this script expects. * * @return the number of parameters */ public int getArgCount() { return parms; } /** * Gets this script registers, i.e. parameters and local variables. * * @return the register names */ public String[] getRegisters() { return namedRegisters != null ? namedRegisters.keySet().toArray(new String[0]) : new String[0]; } /** * Gets this script parameters, i.e. registers assigned before creating local variables. * * @return the parameter names */ public String[] getParameters() { if (namedRegisters != null && parms > 0) { String[] pa = new String[parms]; int p = 0; for (Map.Entry<String, Integer> entry : namedRegisters.entrySet()) { if (entry.getValue().intValue() < parms) { pa[p++] = entry.getKey(); } } return pa; } else { return null; } } /** * Gets this script local variable, i.e. registers assigned to local variables. * * @return the parameter names */ public String[] getLocalVariables() { if (namedRegisters != null && parms > 0) { String[] pa = new String[parms]; int p = 0; for (Map.Entry<String, Integer> entry : namedRegisters.entrySet()) { if (entry.getValue().intValue() >= parms) { pa[p++] = entry.getKey(); } } return pa; } else { return null; } } } /** * A call frame, created from a scope, stores the arguments and local variables as "registers". * * @since 2.1 */ public static final class Frame { /** * Registers or arguments. */ private Object[] registers = null; /** * Parameter and argument names if any. */ private String[] parameters = null; /** * Creates a new frame. * * @param r the registers * @param p the parameters */ Frame(Object[] r, String[] p) { registers = r; parameters = p; } /** * @return the registers */ public Object[] getRegisters() { return registers; } /** * @return the parameters */ public String[] getParameters() { return parameters; } } /** * 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 * @deprecated Use {@link #parse(CharSequence, JexlInfo, Scope)} instead */ @Deprecated protected ASTJexlScript parse(CharSequence expression, JexlInfo info) { return parse(expression, info, null); } /** * Parses an expression. * * @param expression the expression to parse * @param info debug information structure * @param frame the script frame to use * @return the parsed tree * @throws JexlException if any error occured during parsing */ protected ASTJexlScript parse(CharSequence expression, JexlInfo info, Scope frame) { String expr = cleanExpression(expression); ASTJexlScript script = null; JexlInfo dbgInfo = null; synchronized (parser) { if (cache != null) { script = cache.get(expr); if (script != null) { Scope f = script.getScope(); if ((f == null && frame == null) || (f != null && f.equals(frame))) { return script; } } } try { Reader reader = new StringReader(expr); // use first calling method of JexlEngine as debug info if (info == null) { dbgInfo = debugInfo(); } else { dbgInfo = info.debugInfo(); } parser.setFrame(frame); script = parser.parse(reader, dbgInfo); // reaccess in case local variables have been declared frame = parser.getFrame(); if (frame != null) { script.setScope(frame); } if (cache != null) { cache.put(expr, script); } } catch (TokenMgrError xtme) { throw new JexlException.Tokenization(dbgInfo, expression, xtme); } catch (ParseException xparse) { throw new JexlException.Parsing(dbgInfo, expression, xparse); } finally { parser.setFrame(null); } } return script; } /** * 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. * <p>This gathers the class, method and line number of the first calling method * not owned by JexlEngine, UnifiedJEXL or {Script,Expression}Factory.</p> * * @return an Info if debug is set, null otherwise */ protected JexlInfo debugInfo() { DebugInfo 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).debugInfo(); } } return info; } /** * Trims the expression from front and ending spaces. * * @param str expression to clean * @return trimmed expression ending in a semi-colon */ public static 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 local buffer 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 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 } } } }