001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.apache.commons.jexl2;
018    
019    import java.io.BufferedReader;
020    import java.io.File;
021    import java.io.FileReader;
022    import java.io.IOException;
023    import java.io.InputStreamReader;
024    import java.io.StringReader;
025    import java.io.Reader;
026    import java.net.URL;
027    import java.net.URLConnection;
028    import java.lang.ref.SoftReference;
029    import java.lang.reflect.Constructor;
030    import java.util.Map;
031    import java.util.Set;
032    import java.util.Collections;
033    import java.util.Map.Entry;
034    import org.apache.commons.logging.Log;
035    import org.apache.commons.logging.LogFactory;
036    
037    import org.apache.commons.jexl2.parser.ParseException;
038    import org.apache.commons.jexl2.parser.Parser;
039    import org.apache.commons.jexl2.parser.JexlNode;
040    import org.apache.commons.jexl2.parser.TokenMgrError;
041    import org.apache.commons.jexl2.parser.ASTJexlScript;
042    
043    import org.apache.commons.jexl2.introspection.Uberspect;
044    import org.apache.commons.jexl2.introspection.UberspectImpl;
045    import org.apache.commons.jexl2.introspection.JexlMethod;
046    
047    /**
048     * <p>
049     * Creates and evaluates Expression and Script objects.
050     * Determines the behavior of Expressions & Scripts during their evaluation with respect to:
051     * <ul>
052     *  <li>Introspection, see {@link Uberspect}</li>
053     *  <li>Arithmetic & comparison, see {@link JexlArithmetic}</li>
054     *  <li>Error reporting</li>
055     *  <li>Logging</li>
056     * </ul>
057     * </p>
058     * <p>The <code>setSilent</code>and<code>setLenient</code> methods allow to fine-tune an engine instance behavior
059     * according to various error control needs.
060     * </p>
061     * <ul>
062     * <li>When "silent" & "lenient" (not-strict):
063     * <p> 0 & null should be indicators of "default" values so that even in an case of error,
064     * something meaningfull can still be inferred; may be convenient for configurations.
065     * </p>
066     * </li>
067     * <li>When "silent" & "strict":
068     * <p>One should probably consider using null as an error case - ie, every object
069     * manipulated by JEXL should be valued; the ternary operator, especially the '?:' form
070     * can be used to workaround exceptional cases.
071     * Use case could be configuration with no implicit values or defaults.
072     * </p>
073     * </li>
074     * <li>When "not-silent" & "not-strict":
075     * <p>The error control grain is roughly on par with JEXL 1.0</p>
076     * </li>
077     * <li>When "not-silent" & "strict":
078     * <p>The finest error control grain is obtained; it is the closest to Java code -
079     * still augmented by "script" capabilities regarding automated conversions & type matching.
080     * </p>
081     * </li>
082     * </ul>
083     * <p>
084     * Note that methods that evaluate expressions may throw <em>unchecked</em> exceptions;
085     * The {@link JexlException} are thrown in "non-silent" mode but since these are
086     * RuntimeException, user-code <em>should</em> catch them wherever most appropriate.
087     * </p>
088     * @since 2.0
089     */
090    public class JexlEngine {    
091        /**
092         * An empty/static/non-mutable JexlContext used instead of null context.
093         */
094        public static final JexlContext EMPTY_CONTEXT = new JexlContext() {
095            /** {@inheritDoc} */
096            public Object get(String name) {
097                return null;
098            }
099            /** {@inheritDoc} */
100            public boolean has(String name) {
101                return false;
102            }
103            /** {@inheritDoc} */
104            public void set(String name, Object value) {
105                throw new UnsupportedOperationException("Not supported in void context.");
106            }
107        };
108    
109        /**
110         *  Gets the default instance of Uberspect.
111         * <p>This is lazily initialized to avoid building a default instance if there
112         * is no use for it. The main reason for not using the default Uberspect instance is to
113         * be able to use a (low level) introspector created with a given logger
114         * instead of the default one.</p>
115         * <p>Implemented as on demand holder idiom.</p>
116         */
117        private static final class UberspectHolder {
118            /** The default uberspector that handles all introspection patterns. */
119            private static final Uberspect UBERSPECT = new UberspectImpl(LogFactory.getLog(JexlEngine.class));
120            /** Non-instantiable. */
121            private UberspectHolder() {}
122        }
123        
124        /**
125         * The Uberspect instance.
126         */
127        protected final Uberspect uberspect;
128        /**
129         * The JexlArithmetic instance.
130         */
131        protected final JexlArithmetic arithmetic;
132        /**
133         * The Log to which all JexlEngine messages will be logged.
134         */
135        protected final Log logger;
136        /**
137         * The singleton ExpressionFactory also holds a single instance of
138         * {@link Parser}.
139         * When parsing expressions, ExpressionFactory synchronizes on Parser.
140         */
141        protected final Parser parser = new Parser(new StringReader(";")); //$NON-NLS-1$
142        /**
143         * Whether expressions evaluated by this engine will throw exceptions (false) or 
144         * return null (true). Default is false.
145         */
146        protected boolean silent = false;
147        /**
148         * Whether error messages will carry debugging information.
149         */
150        protected boolean debug = true;
151        /**
152         *  The map of 'prefix:function' to object implementing the function.
153         */
154        protected Map<String, Object> functions = Collections.emptyMap();
155        /**
156         * The expression cache.
157         */
158        protected SoftCache<String, ASTJexlScript> cache = null;
159        /**
160         * The default cache load factor.
161         */
162        private static final float LOAD_FACTOR = 0.75f;
163    
164        /**
165         * Creates an engine with default arguments.
166         */
167        public JexlEngine() {
168            this(null, null, null, null);
169        }
170    
171        /**
172         * Creates a JEXL engine using the provided {@link Uberspect}, (@link JexlArithmetic),
173         * a function map and logger.
174         * @param anUberspect to allow different introspection behaviour
175         * @param anArithmetic to allow different arithmetic behaviour
176         * @param theFunctions an optional map of functions (@link setFunctions)
177         * @param log the logger for various messages
178         */
179        public JexlEngine(Uberspect anUberspect, JexlArithmetic anArithmetic, Map<String, Object> theFunctions, Log log) {
180            this.uberspect = anUberspect == null ? getUberspect(log) : anUberspect;
181            if (log == null) {
182                log = LogFactory.getLog(JexlEngine.class);
183            }
184            this.logger = log;
185            this.arithmetic = anArithmetic == null ? new JexlArithmetic(true) : anArithmetic;
186            if (theFunctions != null) {
187                this.functions = theFunctions;
188            }
189        }
190    
191    
192        /**
193         *  Gets the default instance of Uberspect.
194         * <p>This is lazily initialized to avoid building a default instance if there
195         * is no use for it. The main reason for not using the default Uberspect instance is to
196         * be able to use a (low level) introspector created with a given logger
197         * instead of the default one.</p>
198         * @param logger the logger to use for the underlying Uberspect
199         * @return Uberspect the default uberspector instance.
200         */
201        public static Uberspect getUberspect(Log logger) {
202            if (logger == null || logger.equals(LogFactory.getLog(JexlEngine.class))) {
203                return UberspectHolder.UBERSPECT;
204            }
205            return new UberspectImpl(logger);
206        }
207    
208        /**
209         * Gets this engine underlying uberspect.
210         * @return the uberspect
211         */
212        public Uberspect getUberspect() {
213            return uberspect;
214        }
215    
216        /**
217         * Sets whether this engine reports debugging information when error occurs.
218         * <p>This method is <em>not</em> thread safe; it should be called as an optional step of the JexlEngine
219         * initialization code before expression creation &amp; evaluation.</p>
220         * @see JexlEngine#setSilent
221         * @see JexlEngine#setLenient
222         * @param flag true implies debug is on, false implies debug is off.
223         */
224        public void setDebug(boolean flag) {
225            this.debug = flag;
226        }
227    
228        /**
229         * Checks whether this engine is in debug mode.
230         * @return true if debug is on, false otherwise
231         */
232        public boolean isDebug() {
233            return this.debug;
234        }
235    
236        /**
237         * Sets whether this engine throws JexlException during evaluation when an error is triggered.
238         * <p>This method is <em>not</em> thread safe; it should be called as an optional step of the JexlEngine
239         * initialization code before expression creation &amp; evaluation.</p>
240         * @see JexlEngine#setDebug
241         * @see JexlEngine#setLenient
242         * @param flag true means no JexlException will occur, false allows them
243         */
244        public void setSilent(boolean flag) {
245            this.silent = flag;
246        }
247    
248        /**
249         * Checks whether this engine throws JexlException during evaluation.
250         * @return true if silent, false (default) otherwise
251         */
252        public boolean isSilent() {
253            return this.silent;
254        }
255    
256        /**
257         * Sets whether this engine triggers errors during evaluation when null is used as
258         * an operand.
259         * <p>This method is <em>not</em> thread safe; it should be called as an optional step of the JexlEngine
260         * initialization code before expression creation &amp; evaluation.</p>
261         * @see JexlEngine#setSilent
262         * @see JexlEngine#setDebug
263         * @param flag true means no JexlException will occur, false allows them
264         */
265        public void setLenient(boolean flag) {
266            this.arithmetic.setLenient(flag);
267        }
268    
269        /**
270         * Checks whether this engine triggers errors during evaluation when null is used as
271         * an operand.
272         * @return true if lenient, false if strict
273         */
274        public boolean isLenient() {
275            return this.arithmetic.isLenient();
276        }
277    
278        /**
279         * Sets the class loader used to discover classes in 'new' expressions.
280         * <p>This method should be called as an optional step of the JexlEngine
281         * initialization code before expression creation &amp; evaluation.</p>
282         * @param loader the class loader to use
283         */
284        public void setClassLoader(ClassLoader loader) {
285            uberspect.setClassLoader(loader);
286        }
287    
288        /**
289         * Sets a cache of the defined size for expressions.
290         * @param size if not strictly positive, no cache is used.
291         */
292        public void setCache(int size) {
293            // since the cache is only used during parse, use same sync object
294            synchronized (parser) {
295                if (size <= 0) {
296                    cache = null;
297                } else if (cache == null || cache.size() != size) {
298                    cache = new SoftCache<String, ASTJexlScript>(size);
299                }
300            }
301        }
302    
303        /**
304         * Sets the map of function namespaces.
305         * <p>
306         * This method is <em>not</em> thread safe; it should be called as an optional step of the JexlEngine
307         * initialization code before expression creation &amp; evaluation.
308         * </p>
309         * <p>
310         * Each entry key is used as a prefix, each entry value used as a bean implementing
311         * methods; an expression like 'nsx:method(123)' will thus be solved by looking at
312         * a registered bean named 'nsx' that implements method 'method' in that map.
313         * If all methods are static, you may use the bean class instead of an instance as value.
314         * </p>
315         * <p>
316         * If the entry value is a class that has one contructor taking a JexlContext as argument, an instance
317         * of the namespace will be created at evaluation time. It might be a good idea to derive a JexlContext
318         * to carry the information used by the namespace to avoid variable space pollution and strongly type
319         * the constructor with this specialized JexlContext.
320         * </p>
321         * <p>
322         * The key or prefix allows to retrieve the bean that plays the role of the namespace.
323         * If the prefix is null, the namespace is the top-level namespace allowing to define
324         * top-level user defined functions ( ie: myfunc(...) )
325         * </p>
326         * @param funcs the map of functions that should not mutate after the call; if null
327         * is passed, the empty collection is used.
328         */
329        public void setFunctions(Map<String, Object> funcs) {
330            functions = funcs != null ? funcs : Collections.<String, Object>emptyMap();
331        }
332    
333        /**
334         * Retrieves the map of function namespaces.
335         *
336         * @return the map passed in setFunctions or the empty map if the
337         * original was null.
338         */
339        public Map<String, Object> getFunctions() {
340            return functions;
341        }
342    
343        /**
344         * An overridable through covariant return Expression creator.
345         * @param text the script text
346         * @param tree the parse AST tree
347         * @return the script instance
348         */
349        protected Expression createExpression(ASTJexlScript tree, String text) {
350            return new ExpressionImpl(this, text, tree);
351        }
352        
353        /**
354         * Creates an Expression from a String containing valid
355         * JEXL syntax.  This method parses the expression which
356         * must contain either a reference or an expression.
357         * @param expression A String containing valid JEXL syntax
358         * @return An Expression object which can be evaluated with a JexlContext
359         * @throws JexlException An exception can be thrown if there is a problem
360         *      parsing this expression, or if the expression is neither an
361         *      expression nor a reference.
362         */
363        public Expression createExpression(String expression) {
364            return createExpression(expression, null);
365        }
366    
367        /**
368         * Creates an Expression from a String containing valid
369         * JEXL syntax.  This method parses the expression which
370         * must contain either a reference or an expression.
371         * @param expression A String containing valid JEXL syntax
372         * @return An Expression object which can be evaluated with a JexlContext
373         * @param info An info structure to carry debugging information if needed
374         * @throws JexlException An exception can be thrown if there is a problem
375         *      parsing this expression, or if the expression is neither an
376         *      expression or a reference.
377         */
378        public Expression createExpression(String expression, JexlInfo info) {
379            // Parse the expression
380            ASTJexlScript tree = parse(expression, info);
381            if (tree.jjtGetNumChildren() > 1) {
382                logger.warn("The JEXL Expression created will be a reference"
383                          + " to the first expression from the supplied script: \"" + expression + "\" ");
384            }
385            return createExpression(tree, expression);
386        }
387    
388        /**
389         * Creates a Script from a String containing valid JEXL syntax.
390         * This method parses the script which validates the syntax.
391         *
392         * @param scriptText A String containing valid JEXL syntax
393         * @return A {@link Script} which can be executed using a {@link JexlContext}.
394         * @throws JexlException if there is a problem parsing the script.
395         */
396        public Script createScript(String scriptText) {
397            return createScript(scriptText, null);
398        }
399    
400        /**
401         * Creates a Script from a String containing valid JEXL syntax.
402         * This method parses the script which validates the syntax.
403         *
404         * @param scriptText A String containing valid JEXL syntax
405         * @param info An info structure to carry debugging information if needed
406         * @return A {@link Script} which can be executed using a {@link JexlContext}.
407         * @throws JexlException if there is a problem parsing the script.
408         */
409        public Script createScript(String scriptText, JexlInfo info) {
410            if (scriptText == null) {
411                throw new NullPointerException("scriptText is null");
412            }
413            // Parse the expression
414            ASTJexlScript tree = parse(scriptText, info);
415            return createScript(tree, scriptText);
416        }
417    
418        /**
419         * An overridable through covariant return Script creator.
420         * @param text the script text
421         * @param tree the parse AST tree
422         * @return the script instance
423         */
424        protected Script createScript(ASTJexlScript tree, String text) {
425            return new ExpressionImpl(this, text, tree);
426        }
427        
428        /**
429         * Creates a Script from a {@link File} containing valid JEXL syntax.
430         * This method parses the script and validates the syntax.
431         *
432         * @param scriptFile A {@link File} containing valid JEXL syntax.
433         *      Must not be null. Must be a readable file.
434         * @return A {@link Script} which can be executed with a
435         *      {@link JexlContext}.
436         * @throws IOException if there is a problem reading the script.
437         * @throws JexlException if there is a problem parsing the script.
438         */
439        public Script createScript(File scriptFile) throws IOException {
440            if (scriptFile == null) {
441                throw new NullPointerException("scriptFile is null");
442            }
443            if (!scriptFile.canRead()) {
444                throw new IOException("Can't read scriptFile (" + scriptFile.getCanonicalPath() + ")");
445            }
446            BufferedReader reader = new BufferedReader(new FileReader(scriptFile));
447            JexlInfo info = null;
448            if (debug) {
449                info = createInfo(scriptFile.getName(), 0, 0);
450            }
451            return createScript(readerToString(reader), info);
452        }
453    
454        /**
455         * Creates a Script from a {@link URL} containing valid JEXL syntax.
456         * This method parses the script and validates the syntax.
457         *
458         * @param scriptUrl A {@link URL} containing valid JEXL syntax.
459         *      Must not be null. Must be a readable file.
460         * @return A {@link Script} which can be executed with a
461         *      {@link JexlContext}.
462         * @throws IOException if there is a problem reading the script.
463         * @throws JexlException if there is a problem parsing the script.
464         */
465        public Script createScript(URL scriptUrl) throws IOException {
466            if (scriptUrl == null) {
467                throw new NullPointerException("scriptUrl is null");
468            }
469            URLConnection connection = scriptUrl.openConnection();
470    
471            BufferedReader reader = new BufferedReader(
472                    new InputStreamReader(connection.getInputStream()));
473            JexlInfo info = null;
474            if (debug) {
475                info = createInfo(scriptUrl.toString(), 0, 0);
476            }
477            return createScript(readerToString(reader), info);
478        }
479    
480        /**
481         * Accesses properties of a bean using an expression.
482         * <p>
483         * jexl.get(myobject, "foo.bar"); should equate to
484         * myobject.getFoo().getBar(); (or myobject.getFoo().get("bar"))
485         * </p>
486         * <p>
487         * If the JEXL engine is silent, errors will be logged through its logger as warning.
488         * </p>
489         * @param bean the bean to get properties from
490         * @param expr the property expression
491         * @return the value of the property
492         * @throws JexlException if there is an error parsing the expression or during evaluation
493         */
494        public Object getProperty(Object bean, String expr) {
495            return getProperty(null, bean, expr);
496        }
497    
498        /**
499         * Accesses properties of a bean using an expression.
500         * <p>
501         * If the JEXL engine is silent, errors will be logged through its logger as warning.
502         * </p>
503         * @param context the evaluation context
504         * @param bean the bean to get properties from
505         * @param expr the property expression
506         * @return the value of the property
507         * @throws JexlException if there is an error parsing the expression or during evaluation
508         */
509        public Object getProperty(JexlContext context, Object bean, String expr) {
510            if (context == null) {
511                context = EMPTY_CONTEXT;
512            }
513            // lets build 1 unique & unused identifiers wrt context
514            String r0 = "$0";
515            for (int s = 0; context.has(r0); ++s) {
516                r0 = r0 + s;
517            }
518            expr = r0 + (expr.charAt(0) == '[' ? "" : ".") + expr + ";";
519            try {
520                JexlNode tree = parse(expr, null);
521                JexlNode node = tree.jjtGetChild(0);
522                Interpreter interpreter = createInterpreter(context);
523                // ensure 4 objects in register array
524                Object[] r = {r0, bean, r0, bean};
525                interpreter.setRegisters(r);
526                return node.jjtAccept(interpreter, null);
527            } catch (JexlException xjexl) {
528                if (silent) {
529                    logger.warn(xjexl.getMessage(), xjexl.getCause());
530                    return null;
531                }
532                throw xjexl;
533            }
534        }
535    
536        /**
537         * Assign properties of a bean using an expression.
538         * <p>
539         * jexl.set(myobject, "foo.bar", 10); should equate to
540         * myobject.getFoo().setBar(10); (or myobject.getFoo().put("bar", 10) )
541         * </p>
542         * <p>
543         * If the JEXL engine is silent, errors will be logged through its logger as warning.
544         * </p>
545         * @param bean the bean to set properties in
546         * @param expr the property expression
547         * @param value the value of the property
548         * @throws JexlException if there is an error parsing the expression or during evaluation
549         */
550        public void setProperty(Object bean, String expr, Object value) {
551            setProperty(null, bean, expr, value);
552        }
553    
554        /**
555         * Assign properties of a bean using an expression.
556         * <p>
557         * If the JEXL engine is silent, errors will be logged through its logger as warning.
558         * </p>
559         * @param context the evaluation context
560         * @param bean the bean to set properties in
561         * @param expr the property expression
562         * @param value the value of the property
563         * @throws JexlException if there is an error parsing the expression or during evaluation
564         */
565        public void setProperty(JexlContext context, Object bean, String expr, Object value) {
566            if (context == null) {
567                context = EMPTY_CONTEXT;
568            }
569            // lets build 2 unique & unused identifiers wrt context
570            String r0 = "$0", r1 = "$1";
571            for (int s = 0; context.has(r0); ++s) {
572                r0 = r0 + s;
573            }
574            for (int s = 0; context.has(r1); ++s) {
575                r1 = r1 + s;
576            }
577            // synthetize expr
578            expr = r0 + (expr.charAt(0) == '[' ? "" : ".") + expr + "=" + r1 + ";";
579            try {
580                JexlNode tree = parse(expr, null);
581                JexlNode node = tree.jjtGetChild(0);
582                Interpreter interpreter = createInterpreter(context);
583                // set the registers
584                Object[] r = {r0, bean, r1, value};
585                interpreter.setRegisters(r);
586                node.jjtAccept(interpreter, null);
587            } catch (JexlException xjexl) {
588                if (silent) {
589                    logger.warn(xjexl.getMessage(), xjexl.getCause());
590                    return;
591                }
592                throw xjexl;
593            }
594        }
595    
596        /**
597         * Invokes an object's method by name and arguments.
598         * @param obj the method's invoker object
599         * @param meth the method's name
600         * @param args the method's arguments
601         * @return the method returned value or null if it failed and engine is silent
602         * @throws JexlException if method could not be found or failed and engine is not silent
603         */
604        public Object invokeMethod(Object obj, String meth, Object... args) {
605            JexlException xjexl = null;
606            Object result = null;
607            JexlInfo info = debugInfo();
608            try {
609                JexlMethod method = uberspect.getMethod(obj, meth, args, info);
610                if (method == null && arithmetic.narrowArguments(args)) {
611                    method = uberspect.getMethod(obj, meth, args, info);
612                }
613                if (method != null) {
614                    result = method.invoke(obj, args);
615                } else {
616                    xjexl = new JexlException(info, "failed finding method " + meth);
617                }
618            } catch (Exception xany) {
619                xjexl = new JexlException(info, "failed executing method " + meth, xany);
620            } finally {
621                if (xjexl != null) {
622                    if (silent) {
623                        logger.warn(xjexl.getMessage(), xjexl.getCause());
624                        return null;
625                    }
626                    throw xjexl;
627                }
628            }
629            return result;
630        }
631    
632        /**
633         * Creates a new instance of an object using the most appropriate constructor
634         * based on the arguments.
635         * @param <T> the type of object
636         * @param clazz the class to instantiate
637         * @param args the constructor arguments
638         * @return the created object instance or null on failure when silent
639         */
640        public <T> T newInstance(Class<? extends T> clazz, Object...args) {
641            return clazz.cast(doCreateInstance(clazz, args));
642        }
643    
644        /**
645         * Creates a new instance of an object using the most appropriate constructor
646         * based on the arguments.
647         * @param clazz the name of the class to instantiate resolved through this engine's class loader
648         * @param args the constructor arguments
649         * @return the created object instance or null on failure when silent
650         */
651        public Object newInstance(String clazz, Object...args) {
652           return doCreateInstance(clazz, args);
653        }
654    
655        /**
656         * Creates a new instance of an object using the most appropriate constructor
657         * based on the arguments.
658         * @param clazz the class to instantiate
659         * @param args the constructor arguments
660         * @return the created object instance or null on failure when silent
661         */
662        protected Object doCreateInstance(Object clazz, Object...args) {
663            JexlException xjexl = null;
664            Object result = null;
665            JexlInfo info = debugInfo();
666            try {
667                Constructor<?> ctor = uberspect.getConstructor(clazz, args, info);
668                if (ctor == null && arithmetic.narrowArguments(args)) {
669                    ctor = uberspect.getConstructor(clazz, args, info);
670                }
671                if (ctor != null) {
672                    result = ctor.newInstance(args);
673                } else {
674                    xjexl = new JexlException(info, "failed finding constructor for " + clazz.toString());
675                }
676            } catch (Exception xany) {
677                xjexl = new JexlException(info, "failed executing constructor for " + clazz.toString(), xany);
678            } finally {
679                if (xjexl != null) {
680                    if (silent) {
681                        logger.warn(xjexl.getMessage(), xjexl.getCause());
682                        return null;
683                    }
684                    throw xjexl;
685                }
686            }
687            return result;
688        }
689    
690        /**
691         * Creates an interpreter.
692         * @param context a JexlContext; if null, the EMPTY_CONTEXT is used instead.
693         * @return an Interpreter
694         */
695        protected Interpreter createInterpreter(JexlContext context) {
696            if (context == null) {
697                context = EMPTY_CONTEXT;
698            }
699            return new Interpreter(this, context);
700        }
701    
702        /**
703         * A soft reference on cache.
704         * <p>The cache is held through a soft reference, allowing it to be GCed under
705         * memory pressure.</p>
706         * @param <K> the cache key entry type
707         * @param <V> the cache key value type
708         */
709        protected class SoftCache<K, V> {
710            /**
711             * The cache size.
712             */
713            private final int size;
714            /**
715             * The soft reference to the cache map.
716             */
717            private SoftReference<Map<K, V>> ref = null;
718    
719            /**
720             * Creates a new instance of a soft cache.
721             * @param theSize the cache size
722             */
723            SoftCache(int theSize) {
724                size = theSize;
725            }
726    
727            /**
728             * Returns the cache size.
729             * @return the cache size
730             */
731            int size() {
732                return size;
733            }
734    
735            /**
736             * Produces the cache entry set.
737             * @return the cache entry set
738             */
739            Set<Entry<K, V>> entrySet() {
740                Map<K, V> map = ref != null ? ref.get() : null;
741                return map != null ? map.entrySet() : Collections.<Entry<K, V>>emptySet();
742            }
743    
744            /**
745             * Gets a value from cache.
746             * @param key the cache entry key
747             * @return the cache entry value
748             */
749            V get(K key) {
750                final Map<K, V> map = ref != null ? ref.get() : null;
751                return map != null ? map.get(key) : null;
752            }
753    
754            /**
755             * Puts a value in cache.
756             * @param key the cache entry key
757             * @param script the cache entry value
758             */
759            void put(K key, V script) {
760                Map<K, V> map = ref != null ? ref.get() : null;
761                if (map == null) {
762                    map = createCache(size);
763                    ref = new SoftReference<Map<K, V>>(map);
764                }
765                map.put(key, script);
766            }
767        }
768    
769        /**
770         * Creates a cache.
771         * @param <K> the key type
772         * @param <V> the value type
773         * @param cacheSize the cache size, must be > 0
774         * @return a Map usable as a cache bounded to the given size
775         */
776        protected <K, V> Map<K, V> createCache(final int cacheSize) {
777            return new java.util.LinkedHashMap<K, V>(cacheSize, LOAD_FACTOR, true) {
778                /** Serial version UID. */
779                private static final long serialVersionUID = 3801124242820219131L;
780    
781                @Override
782                protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
783                    return size() > cacheSize;
784                }
785            };
786        }
787    
788        /**
789         * Parses an expression.
790         * @param expression the expression to parse
791         * @param info debug information structure
792         * @return the parsed tree
793         * @throws JexlException if any error occured during parsing
794         */
795        protected ASTJexlScript parse(CharSequence expression, JexlInfo info) {
796            String expr = cleanExpression(expression);
797            ASTJexlScript tree = null;
798            synchronized (parser) {
799                if (cache != null) {
800                    tree = cache.get(expr);
801                    if (tree != null) {
802                        return tree;
803                    }
804                }
805                try {
806                    Reader reader = new StringReader(expr);
807                    // use first calling method of JexlEngine as debug info
808                    if (info == null) {
809                        info = debugInfo();
810                    }
811                    tree = parser.parse(reader, info);
812                    if (cache != null) {
813                        cache.put(expr, tree);
814                    }
815                } catch (TokenMgrError xtme) {
816                    throw new JexlException(info, "tokenization failed", xtme);
817                } catch (ParseException xparse) {
818                    throw new JexlException(info, "parsing failed", xparse);
819                }
820            }
821            return tree;
822        }
823    
824        /**
825         * Creates a JexlInfo instance.
826         * @param fn url/file name
827         * @param l line number
828         * @param c column number
829         * @return a JexlInfo instance
830         */
831        protected JexlInfo createInfo(String fn, int l, int c) {
832            return new DebugInfo(fn, l, c);
833        }
834    
835        /**
836         * Creates and fills up debugging information.
837         * <p>This gathers the class, method and line number of the first calling method
838         * not owned by JexlEngine, UnifiedJEXL or {Script,Expression}Factory.</p>
839         * @return an Info if debug is set, null otherwise
840         */
841        protected JexlInfo debugInfo() {
842            JexlInfo info = null;
843            if (debug) {
844                Throwable xinfo = new Throwable();
845                xinfo.fillInStackTrace();
846                StackTraceElement[] stack = xinfo.getStackTrace();
847                StackTraceElement se = null;
848                Class<?> clazz = getClass();
849                for (int s = 1; s < stack.length; ++s, se = null) {
850                    se = stack[s];
851                    String className = se.getClassName();
852                    if (!className.equals(clazz.getName())) {
853                        // go deeper if called from JexlEngine or UnifiedJEXL
854                        if (className.equals(JexlEngine.class.getName())) {
855                            clazz = JexlEngine.class;
856                        } else if (className.equals(UnifiedJEXL.class.getName())) {
857                            clazz = UnifiedJEXL.class;
858                        } else {
859                            break;
860                        }
861                    }
862                }
863                if (se != null) {
864                    info = createInfo(se.getClassName() + "." + se.getMethodName(), se.getLineNumber(), 0);
865                }
866            }
867            return info;
868        }
869    
870        /**
871         * Trims the expression from front & ending spaces.
872         * @param str expression to clean
873         * @return trimmed expression ending in a semi-colon
874         */
875        public static final String cleanExpression(CharSequence str) {
876            if (str != null) {
877                int start = 0;
878                int end = str.length();
879                if (end > 0) {
880                    // trim front spaces
881                    while (start < end && str.charAt(start) == ' ') {
882                        ++start;
883                    }
884                    // trim ending spaces
885                    while (end > 0 && str.charAt(end - 1) == ' ') {
886                        --end;
887                    }
888                    return str.subSequence(start, end).toString();
889                }
890                return "";
891            }
892            return null;
893        }
894    
895        /**
896         * Read from a reader into a StringBuffer and return a String with
897         * the contents of the reader.
898         * @param scriptReader to be read.
899         * @return the contents of the reader as a String.
900         * @throws IOException on any error reading the reader.
901         */
902        public static final String readerToString(Reader scriptReader) throws IOException {
903            StringBuilder buffer = new StringBuilder();
904            BufferedReader reader;
905            if (scriptReader instanceof BufferedReader) {
906                reader = (BufferedReader) scriptReader;
907            } else {
908                reader = new BufferedReader(scriptReader);
909            }
910            try {
911                String line;
912                while ((line = reader.readLine()) != null) {
913                    buffer.append(line).append('\n');
914                }
915                return buffer.toString();
916            } finally {
917                try {
918                    reader.close();
919                } catch(IOException xio) {
920                    // ignore
921                }
922            }
923    
924        }
925    }