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