    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.util.ArrayList;
    030    import java.util.Map;
    031    import java.util.Set;
    032    import java.util.Collections;
    033    import java.util.LinkedHashMap;
    034    import java.util.LinkedHashSet;
    035    import java.util.List;
    036    import java.util.Map.Entry;
    037    import org.apache.commons.logging.Log;
    038    import org.apache.commons.logging.LogFactory;
    039
    040    import org.apache.commons.jexl2.parser.ParseException;
    041    import org.apache.commons.jexl2.parser.Parser;
    042    import org.apache.commons.jexl2.parser.JexlNode;
    043    import org.apache.commons.jexl2.parser.TokenMgrError;
    044    import org.apache.commons.jexl2.parser.ASTJexlScript;
    045
    046    import org.apache.commons.jexl2.introspection.Uberspect;
    047    import org.apache.commons.jexl2.introspection.UberspectImpl;
    048    import org.apache.commons.jexl2.introspection.JexlMethod;
    049    import org.apache.commons.jexl2.parser.ASTArrayAccess;
    050    import org.apache.commons.jexl2.parser.ASTIdentifier;
    051    import org.apache.commons.jexl2.parser.ASTReference;
    052
    053    /**
    054     * <p>
    055     * Creates and evaluates Expression and Script objects.
    056     * Determines the behavior of Expressions & Scripts during their evaluation with respect to:
    057     * <ul>
    058     *  <li>Introspection, see {@link Uberspect}</li>
    059     *  <li>Arithmetic & comparison, see {@link JexlArithmetic}</li>
    060     *  <li>Error reporting</li>
    061     *  <li>Logging</li>
    062     * </ul>
    063     * </p>
    064     * <p>The <code>setSilent</code> and <code>setLenient</code> methods allow to fine-tune an engine instance behavior
    065     * according to various error control needs. The lenient/strict flag tells the engine when and if null as operand is
    066     * considered an error, the silent/verbose flag tells the engine what to do with the error
    067     * (log as warning or throw exception).
    068     * </p>
    069     * <ul>
    070     * <li>When "silent" &amp; "lenient":
    071     * <p> 0 & null should be indicators of "default" values so that even in an case of error,
    072     * something meaningfull can still be inferred; may be convenient for configurations.
    073     * </p>
    074     * </li>
    075     * <li>When "silent" &amp; "strict":
    076     * <p>One should probably consider using null as an error case - ie, every object
    077     * manipulated by JEXL should be valued; the ternary operator, especially the '?:' form
    078     * can be used to workaround exceptional cases.
    079     * Use case could be configuration with no implicit values or defaults.
    080     * </p>
    081     * </li>
    082     * <li>When "verbose" &amp; "lenient":
    083     * <p>The error control grain is roughly on par with JEXL 1.0</p>
    084     * </li>
    085     * <li>When "verbose" &amp; "strict":
    086     * <p>The finest error control grain is obtained; it is the closest to Java code -
    087     * still augmented by "script" capabilities regarding automated conversions & type matching.
    088     * </p>
    089     * </li>
    090     * </ul>
    091     * <p>
    092     * Note that methods that evaluate expressions may throw <em>unchecked</em> exceptions;
    093     * The {@link JexlException} are thrown in "non-silent" mode but since these are
    094     * RuntimeException, user-code <em>should</em> catch them wherever most appropriate.
    095     * </p>
    096     * @since 2.0
    097     */
    098    public class JexlEngine {
    099        /**
    100         * An empty/static/non-mutable JexlContext used instead of null context.
    101         */
    102        public static final JexlContext EMPTY_CONTEXT = new JexlContext() {
    103            /** {@inheritDoc} */
    104            public Object get(String name) {
    105                return null;
    106            }
    107
    108            /** {@inheritDoc} */
    109            public boolean has(String name) {
    110                return false;
    111            }
    112
    113            /** {@inheritDoc} */
    114            public void set(String name, Object value) {
    115                throw new UnsupportedOperationException("Not supported in void context.");
    116            }
    117        };
    118
    119        /**
    120         *  Gets the default instance of Uberspect.
    121         * <p>This is lazily initialized to avoid building a default instance if there
    122         * is no use for it. The main reason for not using the default Uberspect instance is to
    123         * be able to use a (low level) introspector created with a given logger
    124         * instead of the default one.</p>
    125         * <p>Implemented as on demand holder idiom.</p>
    126         */
    127        private static final class UberspectHolder {
    128            /** The default uberspector that handles all introspection patterns. */
    129            private static final Uberspect UBERSPECT = new UberspectImpl(LogFactory.getLog(JexlEngine.class));
    130
    131            /** Non-instantiable. */
    132            private UberspectHolder() {
    133            }
    134        }
    135        /**
    136         * The Uberspect instance.
    137         */
    138        protected final Uberspect uberspect;
    139        /**
    140         * The JexlArithmetic instance.
    141         */
    142        protected final JexlArithmetic arithmetic;
    143        /**
    144         * The Log to which all JexlEngine messages will be logged.
    145         */
    146        protected final Log logger;
    147        /**
    148         * The singleton ExpressionFactory also holds a single instance of
    149         * {@link Parser}.
    150         * When parsing expressions, ExpressionFactory synchronizes on Parser.
    151         */
    152        protected final Parser parser = new Parser(new StringReader(";")); //$NON-NLS-1$
    153        /**
    154         * Whether expressions evaluated by this engine will throw exceptions (false) or
    155         * return null (true) on errors. Default is false.
    156         */
    157        // TODO could this be private?
    158        protected volatile boolean silent = false;
    159        /**
    160         * Whether error messages will carry debugging information.
    161         */
    162        // TODO could this be private?
    163        protected volatile boolean debug = true;
    164        /**
    165         *  The map of 'prefix:function' to object implementing the functions.
    166         */
    167        // TODO this could probably be private; is it threadsafe?
    168        protected Map<String, Object> functions = Collections.emptyMap();
    169        /**
    170         * The expression cache.
    171         */
    172        // TODO is this thread-safe? Could it be made private?
    173        protected SoftCache<String, ASTJexlScript> cache = null;
    174        /**
    175         * The default cache load factor.
    176         */
    177        private static final float LOAD_FACTOR = 0.75f;
    178
    179        /**
    180         * Creates an engine with default arguments.
    181         */
    182        public JexlEngine() {
    183            this(null, null, null, null);
    184        }
    185
    186        /**
    187         * Creates a JEXL engine using the provided {@link Uberspect}, (@link JexlArithmetic),
    188         * a function map and logger.
    189         * @param anUberspect to allow different introspection behaviour
    190         * @param anArithmetic to allow different arithmetic behaviour
    191         * @param theFunctions an optional map of functions (@link setFunctions)
    192         * @param log the logger for various messages
    193         */
    194        public JexlEngine(Uberspect anUberspect, JexlArithmetic anArithmetic, Map<String, Object> theFunctions, Log log) {
    195            this.uberspect = anUberspect == null ? getUberspect(log) : anUberspect;
    196            if (log == null) {
    197                log = LogFactory.getLog(JexlEngine.class);
    198            }
    199            this.logger = log;
    200            this.arithmetic = anArithmetic == null ? new JexlArithmetic(true) : anArithmetic;
    201            if (theFunctions != null) {
    202                this.functions = theFunctions;
    203            }
    204        }
    205
    206        /**
    207         *  Gets the default instance of Uberspect.
    208         * <p>This is lazily initialized to avoid building a default instance if there
    209         * is no use for it. The main reason for not using the default Uberspect instance is to
    210         * be able to use a (low level) introspector created with a given logger
    211         * instead of the default one.</p>
    212         * @param logger the logger to use for the underlying Uberspect
    213         * @return Uberspect the default uberspector instance.
    214         */
    215        public static Uberspect getUberspect(Log logger) {
    216            if (logger == null || logger.equals(LogFactory.getLog(JexlEngine.class))) {
    217                return UberspectHolder.UBERSPECT;
    218            }
    219            return new UberspectImpl(logger);
    220        }
    221
    222        /**
    223         * Gets this engine underlying uberspect.
    224         * @return the uberspect
    225         */
    226        public Uberspect getUberspect() {
    227            return uberspect;
    228        }
    229
    230        /**
    231         * Gets this engine underlying arithmetic.
    232         * @return the arithmetic
    233         * @since 2.1
    234         */
    235        public JexlArithmetic getArithmetic() {
    236            return arithmetic;
    237        }
    238
    239        /**
    240         * Sets whether this engine reports debugging information when error occurs.
    241         * <p>This method is <em>not</em> thread safe; it should be called as an optional step of the JexlEngine
    242         * initialization code before expression creation &amp; evaluation.</p>
    243         * @see JexlEngine#setSilent
    244         * @see JexlEngine#setLenient
    245         * @param flag true implies debug is on, false implies debug is off.
    246         */
    247        public void setDebug(boolean flag) {
    248            this.debug = flag;
    249        }
    250
    251        /**
    252         * Checks whether this engine is in debug mode.
    253         * @return true if debug is on, false otherwise
    254         */
    255        public boolean isDebug() {
    256            return this.debug;
    257        }
    258
    259        /**
    260         * Sets whether this engine throws JexlException during evaluation when an error is triggered.
    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#setDebug
    264         * @see JexlEngine#setLenient
    265         * @param flag true means no JexlException will occur, false allows them
    266         */
    267        public void setSilent(boolean flag) {
    268            this.silent = flag;
    269        }
    270
    271        /**
    272         * Checks whether this engine throws JexlException during evaluation.
    273         * @return true if silent, false (default) otherwise
    274         */
    275        public boolean isSilent() {
    276            return this.silent;
    277        }
    278
    279        /**
    280         * Sets whether this engine considers unknown variables, methods and constructors as errors or evaluates them
    281         * as null or zero.
    282         * <p>This method is <em>not</em> thread safe; it should be called as an optional step of the JexlEngine
    283         * initialization code before expression creation &amp; evaluation.</p>
    284         * <p>As of 2.1, you can use a JexlThreadedArithmetic instance to allow the JexlArithmetic
    285         * leniency behavior to be independently specified per thread, whilst still using a single engine.</p>
    286         * @see JexlEngine#setSilent
    287         * @see JexlEngine#setDebug
    288         * @param flag true means no JexlException will occur, false allows them
    289         */
    290        @SuppressWarnings("deprecation")
    291        public void setLenient(boolean flag) {
    292            if (arithmetic instanceof JexlThreadedArithmetic) {
    293                JexlThreadedArithmetic.setLenient(Boolean.valueOf(flag));
    294            } else {
    295                this.arithmetic.setLenient(flag);
    296            }
    297        }
    298
    299        /**
    300         * Checks whether this engine considers unknown variables, methods and constructors as errors.
    301         * @return true if lenient, false if strict
    302         */
    303        public boolean isLenient() {
    304            return arithmetic.isLenient();
    305        }
    306
    307        /**
    308         * Sets whether this engine behaves in strict or lenient mode.
    309         * Equivalent to setLenient(!flag).
    310         * <p>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.</p>
    312         * @param flag true for strict, false for lenient
    313         * @since 2.1
    314         */
    315        public final void setStrict(boolean flag) {
    316            setLenient(!flag);
    317        }
    318
    319        /**
    320         * Checks whether this engine behaves in strict or lenient mode.
    321         * Equivalent to !isLenient().
    322         * @return true for strict, false for lenient
    323         * @since 2.1
    324         */
    325        public final boolean isStrict() {
    326            return !isLenient();
    327        }
    328
    329        /**
    330         * Sets the class loader used to discover classes in 'new' expressions.
    331         * <p>This method should be called as an optional step of the JexlEngine
    332         * initialization code before expression creation &amp; evaluation.</p>
    333         * @param loader the class loader to use
    334         */
    335        public void setClassLoader(ClassLoader loader) {
    336            uberspect.setClassLoader(loader);
    337        }
    338
    339        /**
    340         * Sets a cache for expressions of the defined size.
    341         * <p>The cache will contain at most <code>size</code> expressions. Note that
    342         * all JEXL caches are held through SoftReferences and may be garbage-collected.</p>
    343         * @param size if not strictly positive, no cache is used.
    344         */
    345        public void setCache(int size) {
    346            // since the cache is only used during parse, use same sync object
    347            synchronized (parser) {
    348                if (size <= 0) {
    349                    cache = null;
    350                } else if (cache == null || cache.size() != size) {
    351                    cache = new SoftCache<String, ASTJexlScript>(size);
    352                }
    353            }
    354        }
    355
    356        /**
    357         * Sets the map of function namespaces.
    358         * <p>
    359         * This method is <em>not</em> thread safe; it should be called as an optional step of the JexlEngine
    360         * initialization code before expression creation &amp; evaluation.
    361         * </p>
    362         * <p>
    363         * Each entry key is used as a prefix, each entry value used as a bean implementing
    364         * methods; an expression like 'nsx:method(123)' will thus be solved by looking at
    365         * a registered bean named 'nsx' that implements method 'method' in that map.
    366         * If all methods are static, you may use the bean class instead of an instance as value.
    367         * </p>
    368         * <p>
    369         * If the entry value is a class that has one contructor taking a JexlContext as argument, an instance
    370         * of the namespace will be created at evaluation time. It might be a good idea to derive a JexlContext
    371         * to carry the information used by the namespace to avoid variable space pollution and strongly type
    372         * the constructor with this specialized JexlContext.
    373         * </p>
    374         * <p>
    375         * The key or prefix allows to retrieve the bean that plays the role of the namespace.
    376         * If the prefix is null, the namespace is the top-level namespace allowing to define
    377         * top-level user defined functions ( ie: myfunc(...) )
    378         * </p>
    379         * <p>Note that the JexlContext is also used to try to solve top-level functions. This allows ObjectContext
    380         * derived instances to call methods on the wrapped object.</p>
    381         * @param funcs the map of functions that should not mutate after the call; if null
    382         * is passed, the empty collection is used.
    383         */
    384        public void setFunctions(Map<String, Object> funcs) {
    385            functions = funcs != null ? funcs : Collections.<String, Object>emptyMap();
    386        }
    387
    388        /**
    389         * Retrieves the map of function namespaces.
    390         *
    391         * @return the map passed in setFunctions or the empty map if the
    392         * original was null.
    393         */
    394        public Map<String, Object> getFunctions() {
    395            return functions;
    396        }
    397
    398        /**
    399         * An overridable through covariant return Expression creator.
    400         * @param text the script text
    401         * @param tree the parse AST tree
    402         * @return the script instance
    403         */
    404        protected Expression createExpression(ASTJexlScript tree, String text) {
    405            return new ExpressionImpl(this, text, tree);
    406        }
    407
    408        /**
    409         * Creates an Expression from a String containing valid
    410         * JEXL syntax.  This method parses the expression which
    411         * must contain either a reference or an expression.
    412         * @param expression A String containing valid JEXL syntax
    413         * @return An Expression object which can be evaluated with a JexlContext
    414         * @throws JexlException An exception can be thrown if there is a problem
    415         *      parsing this expression, or if the expression is neither an
    416         *      expression nor a reference.
    417         */
    418        public Expression createExpression(String expression) {
    419            return createExpression(expression, null);
    420        }
    421
    422        /**
    423         * Creates an Expression from a String containing valid
    424         * JEXL syntax.  This method parses the expression which
    425         * must contain either a reference or an expression.
    426         * @param expression A String containing valid JEXL syntax
    427         * @return An Expression object which can be evaluated with a JexlContext
    428         * @param info An info structure to carry debugging information if needed
    429         * @throws JexlException An exception can be thrown if there is a problem
    430         *      parsing this expression, or if the expression is neither an
    431         *      expression or a reference.
    432         */
    433        public Expression createExpression(String expression, JexlInfo info) {
    434            // Parse the expression
    435            ASTJexlScript tree = parse(expression, info, null);
    436            if (tree.jjtGetNumChildren() > 1) {
    437                logger.warn("The JEXL Expression created will be a reference"
    438                        + " to the first expression from the supplied script: \"" + expression + "\" ");
    439            }
    440            return createExpression(tree, expression);
    441        }
    442
    443        /**
    444         * Creates a Script from a String containing valid JEXL syntax.
    445         * This method parses the script which validates the syntax.
    446         *
    447         * @param scriptText A String containing valid JEXL syntax
    448         * @return A {@link Script} which can be executed using a {@link JexlContext}.
    449         * @throws JexlException if there is a problem parsing the script.
    450         */
    451        public Script createScript(String scriptText) {
    452            return createScript(scriptText, null, null);
    453        }
    454
    455        /**
    456         * Creates a Script from a String containing valid JEXL syntax.
    457         * This method parses the script which validates the syntax.
    458         *
    459         * @param scriptText A String containing valid JEXL syntax
    460         * @param info An info structure to carry debugging information if needed
    461         * @return A {@link Script} which can be executed using a {@link JexlContext}.
    462         * @throws JexlException if there is a problem parsing the script.
    463         * @deprecated Use {@link #createScript(String, JexlInfo, String[])}
    464         */
    465        @Deprecated
    466        public Script createScript(String scriptText, JexlInfo info) {
    467            if (scriptText == null) {
    468                throw new NullPointerException("scriptText is null");
    469            }
    470            // Parse the expression
    471            ASTJexlScript tree = parse(scriptText, info);
    472            return createScript(tree, scriptText);
    473        }
    474
    475        /**
    476         * Creates a Script from a String containing valid JEXL syntax.
    477         * This method parses the script which validates the syntax.
    478         *
    479         * @param scriptText A String containing valid JEXL syntax
    480         * @param names the script parameter names
    481         * @return A {@link Script} which can be executed using a {@link JexlContext}.
    482         * @throws JexlException if there is a problem parsing the script.
    483         */
    484        public Script createScript(String scriptText, String... names) {
    485            return createScript(scriptText, null, names);
    486        }
    487
    488        /**
    489         * Creates a Script from a String containing valid JEXL syntax.
    490         * This method parses the script which validates the syntax.
    491         * It uses an array of parameter names that will be resolved during parsing;
    492         * a corresponding array of arguments containing values should be used during evaluation.
    493         *
    494         * @param scriptText A String containing valid JEXL syntax
    495         * @param info An info structure to carry debugging information if needed
    496         * @param names the script parameter names
    497         * @return A {@link Script} which can be executed using a {@link JexlContext}.
    498         * @throws JexlException if there is a problem parsing the script.
    499         * @since 2.1
    500         */
    501        public Script createScript(String scriptText, JexlInfo info, String[] names) {
    502            if (scriptText == null) {
    503                throw new NullPointerException("scriptText is null");
    504            }
    505            // Parse the expression
    506            ASTJexlScript tree = parse(scriptText, info, new Scope(names));
    507            return createScript(tree, scriptText);
    508        }
    509
    510        /**
    511         * An overridable through covariant return Script creator.
    512         * @param text the script text
    513         * @param tree the parse AST tree
    514         * @return the script instance
    515         */
    516        protected Script createScript(ASTJexlScript tree, String text) {
    517            return new ExpressionImpl(this, text, tree);
    518        }
    519
    520        /**
    521         * Creates a Script from a {@link File} containing valid JEXL syntax.
    522         * This method parses the script and validates the syntax.
    523         *
    524         * @param scriptFile A {@link File} containing valid JEXL syntax.
    525         *      Must not be null. Must be a readable file.
    526         * @return A {@link Script} which can be executed with a
    527         *      {@link JexlContext}.
    528         * @throws IOException if there is a problem reading the script.
    529         * @throws JexlException if there is a problem parsing the script.
    530         */
    531        public Script createScript(File scriptFile) throws IOException {
    532            if (scriptFile == null) {
    533                throw new NullPointerException("scriptFile is null");
    534            }
    535            if (!scriptFile.canRead()) {
    536                throw new IOException("Can't read scriptFile (" + scriptFile.getCanonicalPath() + ")");
    537            }
    538            BufferedReader reader = new BufferedReader(new FileReader(scriptFile));
    539            JexlInfo info = null;
    540            if (debug) {
    541                info = createInfo(scriptFile.getName(), 0, 0);
    542            }
    543            return createScript(readerToString(reader), info, null);
    544        }
    545
    546        /**
    547         * Creates a Script from a {@link URL} containing valid JEXL syntax.
    548         * This method parses the script and validates the syntax.
    549         *
    550         * @param scriptUrl A {@link URL} containing valid JEXL syntax.
    551         *      Must not be null. Must be a readable file.
    552         * @return A {@link Script} which can be executed with a
    553         *      {@link JexlContext}.
    554         * @throws IOException if there is a problem reading the script.
    555         * @throws JexlException if there is a problem parsing the script.
    556         */
    557        public Script createScript(URL scriptUrl) throws IOException {
    558            if (scriptUrl == null) {
    559                throw new NullPointerException("scriptUrl is null");
    560            }
    561            URLConnection connection = scriptUrl.openConnection();
    562
    563            BufferedReader reader = new BufferedReader(
    564                    new InputStreamReader(connection.getInputStream()));
    565            JexlInfo info = null;
    566            if (debug) {
    567                info = createInfo(scriptUrl.toString(), 0, 0);
    568            }
    569            return createScript(readerToString(reader), info, null);
    570        }
    571
    572        /**
    573         * Accesses properties of a bean using an expression.
    574         * <p>
    575         * jexl.get(myobject, "foo.bar"); should equate to
    576         * myobject.getFoo().getBar(); (or myobject.getFoo().get("bar"))
    577         * </p>
    578         * <p>
    579         * If the JEXL engine is silent, errors will be logged through its logger as warning.
    580         * </p>
    581         * @param bean the bean to get properties from
    582         * @param expr the property expression
    583         * @return the value of the property
    584         * @throws JexlException if there is an error parsing the expression or during evaluation
    585         */
    586        public Object getProperty(Object bean, String expr) {
    587            return getProperty(null, bean, expr);
    588        }
    589
    590        /**
    591         * Accesses properties of a bean using an expression.
    592         * <p>
    593         * If the JEXL engine is silent, errors will be logged through its logger as warning.
    594         * </p>
    595         * @param context the evaluation context
    596         * @param bean the bean to get properties from
    597         * @param expr the property expression
    598         * @return the value of the property
    599         * @throws JexlException if there is an error parsing the expression or during evaluation
    600         */
    601        public Object getProperty(JexlContext context, Object bean, String expr) {
    602            if (context == null) {
    603                context = EMPTY_CONTEXT;
    604            }
    605            // synthetize expr using register
    606            expr = "#0" + (expr.charAt(0) == '[' ? "" : ".") + expr + ";";
    607            try {
    608                parser.ALLOW_REGISTERS = true;
    609                Scope frame = new Scope("#0");
    610                ASTJexlScript script = parse(expr, null, frame);
    611                JexlNode node = script.jjtGetChild(0);
    612                Interpreter interpreter = createInterpreter(context);
    613                // set frame
    614                interpreter.setFrame(script.createFrame(bean));
    615                return node.jjtAccept(interpreter, null);
    616            } catch (JexlException xjexl) {
    617                if (silent) {
    618                    logger.warn(xjexl.getMessage(), xjexl.getCause());
    619                    return null;
    620                }
    621                throw xjexl;
    622            } finally {
    623                parser.ALLOW_REGISTERS = false;
    624            }
    625        }
    626
    627        /**
    628         * Assign properties of a bean using an expression.
    629         * <p>
    630         * jexl.set(myobject, "foo.bar", 10); should equate to
    631         * myobject.getFoo().setBar(10); (or myobject.getFoo().put("bar", 10) )
    632         * </p>
    633         * <p>
    634         * If the JEXL engine is silent, errors will be logged through its logger as warning.
    635         * </p>
    636         * @param bean the bean to set properties in
    637         * @param expr the property expression
    638         * @param value the value of the property
    639         * @throws JexlException if there is an error parsing the expression or during evaluation
    640         */
    641        public void setProperty(Object bean, String expr, Object value) {
    642            setProperty(null, bean, expr, value);
    643        }
    644
    645        /**
    646         * Assign properties of a bean using an expression.
    647         * <p>
    648         * If the JEXL engine is silent, errors will be logged through its logger as warning.
    649         * </p>
    650         * @param context the evaluation context
    651         * @param bean the bean to set properties in
    652         * @param expr the property expression
    653         * @param value the value of the property
    654         * @throws JexlException if there is an error parsing the expression or during evaluation
    655         */
    656        public void setProperty(JexlContext context, Object bean, String expr, Object value) {
    657            if (context == null) {
    658                context = EMPTY_CONTEXT;
    659            }
    660            // synthetize expr using registers
    661            expr = "#0" + (expr.charAt(0) == '[' ? "" : ".") + expr + "=" + "#1" + ";";
    662            try {
    663                parser.ALLOW_REGISTERS = true;
    664                Scope frame = new Scope("#0", "#1");
    665                ASTJexlScript script = parse(expr, null, frame);
    666                JexlNode node = script.jjtGetChild(0);
    667                Interpreter interpreter = createInterpreter(context);
    668                // set the registers
    669                interpreter.setFrame(script.createFrame(bean, value));
    670                node.jjtAccept(interpreter, null);
    671            } catch (JexlException xjexl) {
    672                if (silent) {
    673                    logger.warn(xjexl.getMessage(), xjexl.getCause());
    674                    return;
    675                }
    676                throw xjexl;
    677            } finally {
    678                parser.ALLOW_REGISTERS = false;
    679            }
    680        }
    681
    682        /**
    683         * Invokes an object's method by name and arguments.
    684         * @param obj the method's invoker object
    685         * @param meth the method's name
    686         * @param args the method's arguments
    687         * @return the method returned value or null if it failed and engine is silent
    688         * @throws JexlException if method could not be found or failed and engine is not silent
    689         */
    690        public Object invokeMethod(Object obj, String meth, Object... args) {
    691            JexlException xjexl = null;
    692            Object result = null;
    693            JexlInfo info = debugInfo();
    694            try {
    695                JexlMethod method = uberspect.getMethod(obj, meth, args, info);
    696                if (method == null && arithmetic.narrowArguments(args)) {
    697                    method = uberspect.getMethod(obj, meth, args, info);
    698                }
    699                if (method != null) {
    700                    result = method.invoke(obj, args);
    701                } else {
    702                    xjexl = new JexlException(info, "failed finding method " + meth);
    703                }
    704            } catch (Exception xany) {
    705                xjexl = new JexlException(info, "failed executing method " + meth, xany);
    706            } finally {
    707                if (xjexl != null) {
    708                    if (silent) {
    709                        logger.warn(xjexl.getMessage(), xjexl.getCause());
    710                        return null;
    711                    }
    712                    throw xjexl;
    713                }
    714            }
    715            return result;
    716        }
    717
    718        /**
    719         * Creates a new instance of an object using the most appropriate constructor
    720         * based on the arguments.
    721         * @param <T> the type of object
    722         * @param clazz the class to instantiate
    723         * @param args the constructor arguments
    724         * @return the created object instance or null on failure when silent
    725         */
    726        public <T> T newInstance(Class<? extends T> clazz, Object... args) {
    727            return clazz.cast(doCreateInstance(clazz, args));
    728        }
    729
    730        /**
    731         * Creates a new instance of an object using the most appropriate constructor
    732         * based on the arguments.
    733         * @param clazz the name of the class to instantiate resolved through this engine's class loader
    734         * @param args the constructor arguments
    735         * @return the created object instance or null on failure when silent
    736         */
    737        public Object newInstance(String clazz, Object... args) {
    738            return doCreateInstance(clazz, args);
    739        }
    740
    741        /**
    742         * Creates a new instance of an object using the most appropriate constructor
    743         * based on the arguments.
    744         * @param clazz the class to instantiate
    745         * @param args the constructor arguments
    746         * @return the created object instance or null on failure when silent
    747         */
    748        protected Object doCreateInstance(Object clazz, Object... args) {
    749            JexlException xjexl = null;
    750            Object result = null;
    751            JexlInfo info = debugInfo();
    752            try {
    753                JexlMethod ctor = uberspect.getConstructorMethod(clazz, args, info);
    754                if (ctor == null && arithmetic.narrowArguments(args)) {
    755                    ctor = uberspect.getConstructorMethod(clazz, args, info);
    756                }
    757                if (ctor != null) {
    758                    result = ctor.invoke(clazz, args);
    759                } else {
    760                    xjexl = new JexlException(info, "failed finding constructor for " + clazz.toString());
    761                }
    762            } catch (Exception xany) {
    763                xjexl = new JexlException(info, "failed executing constructor for " + clazz.toString(), xany);
    764            } finally {
    765                if (xjexl != null) {
    766                    if (silent) {
    767                        logger.warn(xjexl.getMessage(), xjexl.getCause());
    768                        return null;
    769                    }
    770                    throw xjexl;
    771                }
    772            }
    773            return result;
    774        }
    775
    776        /**
    777         * Creates an interpreter.
    778         * @param context a JexlContext; if null, the EMPTY_CONTEXT is used instead.
    779         * @return an Interpreter
    780         */
    781        protected Interpreter createInterpreter(JexlContext context) {
    782            return createInterpreter(context, isStrict(), isSilent());
    783        }
    784
    785        /**
    786         * Creates an interpreter.
    787         * @param context a JexlContext; if null, the EMPTY_CONTEXT is used instead.
    788         * @param strictFlag whether the interpreter runs in strict mode
    789         * @param silentFlag whether the interpreter runs in silent mode
    790         * @return an Interpreter
    791         * @since 2.1
    792         */
    793        protected Interpreter createInterpreter(JexlContext context, boolean strictFlag, boolean silentFlag) {
    794            return new Interpreter(this, context == null ? EMPTY_CONTEXT : context, strictFlag, silentFlag);
    795        }
    796
    797        /**
    798         * A soft reference on cache.
    799         * <p>The cache is held through a soft reference, allowing it to be GCed under
    800         * memory pressure.</p>
    801         * @param <K> the cache key entry type
    802         * @param <V> the cache key value type
    803         */
    804        protected class SoftCache<K, V> {
    805            /**
    806             * The cache size.
    807             */
    808            private final int size;
    809            /**
    810             * The soft reference to the cache map.
    811             */
    812            private SoftReference<Map<K, V>> ref = null;
    813
    814            /**
    815             * Creates a new instance of a soft cache.
    816             * @param theSize the cache size
    817             */
    818            SoftCache(int theSize) {
    819                size = theSize;
    820            }
    821
    822            /**
    823             * Returns the cache size.
    824             * @return the cache size
    825             */
    826            int size() {
    827                return size;
    828            }
    829
    830            /**
    831             * Clears the cache.
    832             */
    833            void clear() {
    834                ref = null;
    835            }
    836
    837            /**
    838             * Produces the cache entry set.
    839             * @return the cache entry set
    840             */
    841            Set<Entry<K, V>> entrySet() {
    842                Map<K, V> map = ref != null ? ref.get() : null;
    843                return map != null ? map.entrySet() : Collections.<Entry<K, V>>emptySet();
    844            }
    845
    846            /**
    847             * Gets a value from cache.
    848             * @param key the cache entry key
    849             * @return the cache entry value
    850             */
    851            V get(K key) {
    852                final Map<K, V> map = ref != null ? ref.get() : null;
    853                return map != null ? map.get(key) : null;
    854            }
    855
    856            /**
    857             * Puts a value in cache.
    858             * @param key the cache entry key
    859             * @param script the cache entry value
    860             */
    861            void put(K key, V script) {
    862                Map<K, V> map = ref != null ? ref.get() : null;
    863                if (map == null) {
    864                    map = createCache(size);
    865                    ref = new SoftReference<Map<K, V>>(map);
    866                }
    867                map.put(key, script);
    868            }
    869        }
    870
    871        /**
    872         * Creates a cache.
    873         * @param <K> the key type
    874         * @param <V> the value type
    875         * @param cacheSize the cache size, must be > 0
    876         * @return a Map usable as a cache bounded to the given size
    877         */
    878        protected <K, V> Map<K, V> createCache(final int cacheSize) {
    879            return new java.util.LinkedHashMap<K, V>(cacheSize, LOAD_FACTOR, true) {
    880                /** Serial version UID. */
    881                private static final long serialVersionUID = 1L;
    882
    883                @Override
    884                protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
    885                    return size() > cacheSize;
    886                }
    887            };
    888        }
    889
    890        /**
    891         * Clears the expression cache.
    892         * @since 2.1
    893         */
    894        public void clearCache() {
    895            synchronized (parser) {
    896                cache.clear();
    897            }
    898        }
    899
    900        /**
    901         * Gets the list of variables accessed by a script.
    902         * <p>This method will visit all nodes of a script and extract all variables whether they
    903         * are written in 'dot' or 'bracketed' notation. (a.b is equivalent to a['b']).</p>
    904         * @param script the script
    905         * @return the set of variables, each as a list of strings (ant-ish variables use more than 1 string)
    906         *         or the empty set if no variables are used
    907         * @since 2.1
    908         */
    909        public Set<List<String>> getVariables(Script script) {
    910            if (script instanceof ExpressionImpl) {
    911                Set<List<String>> refs = new LinkedHashSet<List<String>>();
    912                getVariables(((ExpressionImpl) script).script, refs, null);
    913                return refs;
    914            } else {
    915                return Collections.<List<String>>emptySet();
    916            }
    917        }
    918
    919        /**
    920         * Fills up the list of variables accessed by a node.
    921         * @param node the node
    922         * @param refs the set of variable being filled
    923         * @param ref the current variable being filled
    924         * @since 2.1
    925         */
    926        protected void getVariables(JexlNode node, Set<List<String>> refs, List<String> ref) {
    927            boolean array = node instanceof ASTArrayAccess;
    928            boolean reference = node instanceof ASTReference;
    929            int num = node.jjtGetNumChildren();
    930            if (array || reference) {
    931                List<String> var = ref != null ? ref : new ArrayList<String>();
    932                boolean varf = true;
    933                for (int i = 0; i < num; ++i) {
    934                    JexlNode child = node.jjtGetChild(i);
    935                    if (array) {
    936                        if (child instanceof ASTReference && child.jjtGetNumChildren() == 1) {
    937                            JexlNode desc = child.jjtGetChild(0);
    938                            if (varf && desc.isConstant()) {
    939                                String image = desc.image;
    940                                if (image == null) {
    941                                    var.add(new Debugger().data(desc));
    942                                } else {
    943                                    var.add(image);
    944                                }
    945                            } else if (desc instanceof ASTIdentifier) {
    946                                if (((ASTIdentifier) desc).getRegister() < 0) {
    947                                    List<String> di = new ArrayList<String>(1);
    948                                    di.add(desc.image);
    949                                    refs.add(di);
    950                                }
    951                                var = new ArrayList<String>();
    952                                varf = false;
    953                            }
    954                            continue;
    955                        } else if (child instanceof ASTIdentifier) {
    956                            if (i == 0 && (((ASTIdentifier) child).getRegister() < 0)) {
    957                                var.add(child.image);
    958                            }
    959                            continue;
    960                        }
    961                    } else {//if (reference) {
    962                        if (child instanceof ASTIdentifier) {
    963                            if (((ASTIdentifier) child).getRegister() < 0) {
    964                                var.add(child.image);
    965                            }
    966                            continue;
    967                        }
    968                    }
    969                    getVariables(child, refs, var);
    970                }
    971                if (!var.isEmpty() && var != ref) {
    972                    refs.add(var);
    973                }
    974            } else {
    975                for (int i = 0; i < num; ++i) {
    976                    getVariables(node.jjtGetChild(i), refs, null);
    977                }
    978            }
    979        }
    980
    981        /**
    982         * Gets the array of parameters from a script.
    983         * @param script the script
    984         * @return the parameters which may be empty (but not null) if no parameters were defined
    985         * @since 2.1
    986         */
    987        protected String[] getParameters(Script script) {
    988            if (script instanceof ExpressionImpl) {
    989                return ((ExpressionImpl) script).getParameters();
    990            } else {
    991                return new String[0];
    992            }
    993        }
    994
    995        /**
    996         * Gets the array of local variable from a script.
    997         * @param script the script
    998         * @return the local variables array which may be empty (but not null) if no local variables were defined
    999         * @since 2.1
    1000         */
    1001        protected String[] getLocalVariables(Script script) {
    1002            if (script instanceof ExpressionImpl) {
    1003                return ((ExpressionImpl) script).getLocalVariables();
    1004            } else {
    1005                return new String[0];
    1006            }
    1007        }
    1008
    1009        /**
    1010         * A script scope, stores the declaration of parameters and local variables.
    1011         * @since 2.1
    1012         */
    1013        public static final class Scope {
    1014            /**
    1015             * The number of parameters.
    1016             */
    1017            private final int parms;
    1018            /**
    1019             * The map of named registers aka script parameters.
    1020             * Each parameter is associated to a register and is materialized as an offset in the registers array used
    1021             * during evaluation.
    1022             */
    1023            private Map<String, Integer> namedRegisters = null;
    1024
    1025            /**
    1026             * Creates a new scope with a list of parameters.
    1027             * @param parameters the list of parameters
    1028             */
    1029            public Scope(String... parameters) {
    1030                if (parameters != null) {
    1031                    parms = parameters.length;
    1032                    namedRegisters = new LinkedHashMap<String, Integer>();
    1033                    for (int p = 0; p < parms; ++p) {
    1034                        namedRegisters.put(parameters[p], Integer.valueOf(p));
    1035                    }
    1036                } else {
    1037                    parms = 0;
    1038                }
    1039            }
    1040
    1041            @Override
    1042            public int hashCode() {
    1043                return namedRegisters == null ? 0 : parms ^ namedRegisters.hashCode();
    1044            }
    1045
    1046            @Override
    1047            public boolean equals(Object o) {
    1048                return o instanceof Scope && equals((Scope) o);
    1049            }
    1050
    1051            /**
    1052             * Whether this frame is equal to another.
    1053             * @param frame the frame to compare to
    1054             * @return true if equal, false otherwise
    1055             */
    1056            public boolean equals(Scope frame) {
    1057                if (this == frame) {
    1058                    return true;
    1059                } else if (frame == null || parms != frame.parms) {
    1060                    return false;
    1061                } else if (namedRegisters == null) {
    1062                    return frame.namedRegisters == null;
    1063                } else {
    1064                    return namedRegisters.equals(frame.namedRegisters);
    1065                }
    1066            }
    1067
    1068            /**
    1069             * Checks whether an identifier is a local variable or argument, ie stored in a register.
    1070             * @param name the register name
    1071             * @return the register index
    1072             */
    1073            public Integer getRegister(String name) {
    1074                return namedRegisters != null ? namedRegisters.get(name) : null;
    1075            }
    1076
    1077            /**
    1078             * Declares a local variable.
    1079             * <p>
    1080             * This method creates an new entry in the named register map.
    1081             * </p>
    1082             * @param name the variable name
    1083             * @return the register index storing this variable
    1084             */
    1085            public Integer declareVariable(String name) {
    1086                if (namedRegisters == null) {
    1087                    namedRegisters = new LinkedHashMap<String, Integer>();
    1088                }
    1089                Integer register = namedRegisters.get(name);
    1090                if (register == null) {
    1091                    register = Integer.valueOf(namedRegisters.size());
    1092                    namedRegisters.put(name, register);
    1093                }
    1094                return register;
    1095            }
    1096
    1097            /**
    1098             * Creates a frame by copying values up to the number of parameters.
    1099             * @param values the argument values
    1100             * @return the arguments array
    1101             */
    1102            public Frame createFrame(Object... values) {
    1103                if (namedRegisters != null) {
    1104                    Object[] arguments = new Object[namedRegisters.size()];
    1105                    if (values != null) {
    1106                        System.arraycopy(values, 0, arguments, 0, Math.min(parms, values.length));
    1107                    }
    1108                    return new Frame(arguments, namedRegisters.keySet().toArray(new String[0]));
    1109                } else {
    1110                    return null;
    1111                }
    1112            }
    1113
    1114            /**
    1115             * Gets the (maximum) number of arguments this script expects.
    1116             * @return the number of parameters
    1117             */
    1118            public int getArgCount() {
    1119                return parms;
    1120            }
    1121
    1122            /**
    1123             * Gets this script registers, i.e. parameters and local variables.
    1124             * @return the register names
    1125             */
    1126            public String[] getRegisters() {
    1127                return namedRegisters != null ? namedRegisters.keySet().toArray(new String[0]) : new String[0];
    1128            }
    1129
    1130            /**
    1131             * Gets this script parameters, i.e. registers assigned before creating local variables.
    1132             * @return the parameter names
    1133             */
    1134            public String[] getParameters() {
    1135                if (namedRegisters != null && parms > 0) {
    1136                    String[] pa = new String[parms];
    1137                    int p = 0;
    1138                    for (Map.Entry<String, Integer> entry : namedRegisters.entrySet()) {
    1139                        if (entry.getValue().intValue() < parms) {
    1140                            pa[p++] = entry.getKey();
    1141                        }
    1142                    }
    1143                    return pa;
    1144                } else {
    1145                    return null;
    1146                }
    1147            }
    1148
    1149            /**
    1150             * Gets this script local variable, i.e. registers assigned to local variables.
    1151             * @return the parameter names
    1152             */
    1153            public String[] getLocalVariables() {
    1154                if (namedRegisters != null && parms > 0) {
    1155                    String[] pa = new String[parms];
    1156                    int p = 0;
    1157                    for (Map.Entry<String, Integer> entry : namedRegisters.entrySet()) {
    1158                        if (entry.getValue().intValue() >= parms) {
    1159                            pa[p++] = entry.getKey();
    1160                        }
    1161                    }
    1162                    return pa;
    1163                } else {
    1164                    return null;
    1165                }
    1166            }
    1167        }
    1168
    1169        /**
    1170         * A call frame, created from a scope, stores the arguments and local variables as "registers".
    1171         * @since 2.1
    1172         */
    1173        public static final class Frame {
    1174            /** Registers or arguments. */
    1175            private Object[] registers = null;
    1176            /** Parameter and argument names if any. */
    1177            private String[] parameters = null;
    1178
    1179            /**
    1180             * Creates a new frame.
    1181             * @param r the registers
    1182             * @param p the parameters
    1183             */
    1184            Frame(Object[] r, String[] p) {
    1185                registers = r;
    1186                parameters = p;
    1187            }
    1188
    1189            /**
    1190             * @return the registers
    1191             */
    1192            public Object[] getRegisters() {
    1193                return registers;
    1194            }
    1195
    1196            /**
    1197             * @return the parameters
    1198             */
    1199            public String[] getParameters() {
    1200                return parameters;
    1201            }
    1202        }
    1203
    1204        /**
    1205         * Parses an expression.
    1206         * @param expression the expression to parse
    1207         * @param info debug information structure
    1208         * @return the parsed tree
    1209         * @throws JexlException if any error occured during parsing
    1210         * @deprecated Use {@link #parse(CharSequence, JexlInfo, Scope)} instead
    1211         */
    1212        @Deprecated
    1213        protected ASTJexlScript parse(CharSequence expression, JexlInfo info) {
    1214            return parse(expression, info, null);
    1215        }
    1216
    1217        /**
    1218         * Parses an expression.
    1219         * @param expression the expression to parse
    1220         * @param info debug information structure
    1221         * @param frame the script frame to use
    1222         * @return the parsed tree
    1223         * @throws JexlException if any error occured during parsing
    1224         */
    1225        protected ASTJexlScript parse(CharSequence expression, JexlInfo info, Scope frame) {
    1226            String expr = cleanExpression(expression);
    1227            ASTJexlScript script = null;
    1228            JexlInfo dbgInfo = null;
    1229            synchronized (parser) {
    1230                if (cache != null) {
    1231                    script = cache.get(expr);
    1232                    if (script != null) {
    1233                        Scope f = script.getScope();
    1234                        if ((f == null && frame == null) || (f != null && f.equals(frame))) {
    1235                            return script;
    1236                        }
    1237                    }
    1238                }
    1239                try {
    1240                    Reader reader = new StringReader(expr);
    1241                    // use first calling method of JexlEngine as debug info
    1242                    if (info == null) {
    1243                        dbgInfo = debugInfo();
    1244                    } else {
    1245                        dbgInfo = info.debugInfo();
    1246                    }
    1247                    parser.setFrame(frame);
    1248                    script = parser.parse(reader, dbgInfo);
    1249                    // reaccess in case local variables have been declared
    1250                    frame = parser.getFrame();
    1251                    if (frame != null) {
    1252                        script.setScope(frame);
    1253                    }
    1254                    if (cache != null) {
    1255                        cache.put(expr, script);
    1256                    }
    1257                } catch (TokenMgrError xtme) {
    1258                    throw new JexlException.Tokenization(dbgInfo, expression, xtme);
    1259                } catch (ParseException xparse) {
    1260                    throw new JexlException.Parsing(dbgInfo, expression, xparse);
    1261                } finally {
    1262                    parser.setFrame(null);
    1263                }
    1264            }
    1265            return script;
    1266        }
    1267
    1268        /**
    1269         * Creates a JexlInfo instance.
    1270         * @param fn url/file name
    1271         * @param l line number
    1272         * @param c column number
    1273         * @return a JexlInfo instance
    1274         */
    1275        protected JexlInfo createInfo(String fn, int l, int c) {
    1276            return new DebugInfo(fn, l, c);
    1277        }
    1278
    1279        /**
    1280         * Creates and fills up debugging information.
    1281         * <p>This gathers the class, method and line number of the first calling method
    1282         * not owned by JexlEngine, UnifiedJEXL or {Script,Expression}Factory.</p>
    1283         * @return an Info if debug is set, null otherwise
    1284         */
    1285        protected JexlInfo debugInfo() {
    1286            DebugInfo info = null;
    1287            if (debug) {
    1288                Throwable xinfo = new Throwable();
    1289                xinfo.fillInStackTrace();
    1290                StackTraceElement[] stack = xinfo.getStackTrace();
    1291                StackTraceElement se = null;
    1292                Class<?> clazz = getClass();
    1293                for (int s = 1; s < stack.length; ++s, se = null) {
    1294                    se = stack[s];
    1295                    String className = se.getClassName();
    1296                    if (!className.equals(clazz.getName())) {
    1297                        // go deeper if called from JexlEngine or UnifiedJEXL
    1298                        if (className.equals(JexlEngine.class.getName())) {
    1299                            clazz = JexlEngine.class;
    1300                        } else if (className.equals(UnifiedJEXL.class.getName())) {
    1301                            clazz = UnifiedJEXL.class;
    1302                        } else {
    1303                            break;
    1304                        }
    1305                    }
    1306                }
    1307                if (se != null) {
    1308                    info = createInfo(se.getClassName() + "." + se.getMethodName(), se.getLineNumber(), 0).debugInfo();
    1309                }
    1310            }
    1311            return info;
    1312        }
    1313
    1314        /**
    1315         * Trims the expression from front & ending spaces.
    1316         * @param str expression to clean
    1317         * @return trimmed expression ending in a semi-colon
    1318         */
    1319        public static String cleanExpression(CharSequence str) {
    1320            if (str != null) {
    1321                int start = 0;
    1322                int end = str.length();
    1323                if (end > 0) {
    1324                    // trim front spaces
    1325                    while (start < end && str.charAt(start) == ' ') {
    1326                        ++start;
    1327                    }
    1328                    // trim ending spaces
    1329                    while (end > 0 && str.charAt(end - 1) == ' ') {
    1330                        --end;
    1331                    }
    1332                    return str.subSequence(start, end).toString();
    1333                }
    1334                return "";
    1335            }
    1336            return null;
    1337        }
    1338
    1339        /**
    1340         * Read from a reader into a local buffer and return a String with
    1341         * the contents of the reader.
    1342         * @param scriptReader to be read.
    1343         * @return the contents of the reader as a String.
    1344         * @throws IOException on any error reading the reader.
    1345         */
    1346        public static String readerToString(Reader scriptReader) throws IOException {
    1347            StringBuilder buffer = new StringBuilder();
    1348            BufferedReader reader;
    1349            if (scriptReader instanceof BufferedReader) {
    1350                reader = (BufferedReader) scriptReader;
    1351            } else {
    1352                reader = new BufferedReader(scriptReader);
    1353            }
    1354            try {
    1355                String line;
    1356                while ((line = reader.readLine()) != null) {
    1357                    buffer.append(line).append('\n');
    1358                }
    1359                return buffer.toString();
    1360            } finally {
    1361                try {
    1362                    reader.close();
    1363                } catch (IOException xio) {
    1364                    // ignore
    1365                }
    1366            }
    1367
    1368        }
    1369    }