    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
    018package org.apache.commons.jexl3;
    019
    020import org.apache.commons.jexl3.internal.Debugger;
    021import org.apache.commons.jexl3.parser.JavaccError;
    022import org.apache.commons.jexl3.parser.JexlNode;
    023import org.apache.commons.jexl3.parser.ParseException;
    024import org.apache.commons.jexl3.parser.TokenMgrException;
    025
    026import java.lang.reflect.InvocationTargetException;
    027import java.lang.reflect.UndeclaredThrowableException;
    028
    029import java.util.ArrayList;
    030import java.util.List;
    031import java.util.Objects;
    032
    033import java.io.BufferedReader;
    034import java.io.IOException;
    035import java.io.StringReader;
    036
    037/**
    038 * Wraps any error that might occur during interpretation of a script or expression.
    039 *
    040 * @since 2.0
    041 */
    042public class JexlException extends RuntimeException {
    043    private static final long serialVersionUID = 20210606123900L;
    044
    045    /** The point of origin for this exception. */
    046    private final transient JexlNode mark;
    047
    048    /** The debug info. */
    049    private final transient JexlInfo info;
    050
    051    /** Maximum number of characters around exception location. */
    052    private static final int MAX_EXCHARLOC = 42;
    053
    054
    055    /**
    056     * Creates a new JexlException.
    057     *
    058     * @param node the node causing the error
    059     * @param msg  the error message
    060     */
    061    public JexlException(final JexlNode node, final String msg) {
    062        this(node, msg, null);
    063    }
    064
    065    /**
    066     * Creates a new JexlException.
    067     *
    068     * @param node  the node causing the error
    069     * @param msg   the error message
    070     * @param cause the exception causing the error
    071     */
    072    public JexlException(final JexlNode node, final String msg, final Throwable cause) {
    073        this(node, msg != null ? msg : "", unwrap(cause), true);
    074    }
    075
    076    /**
    077     * Creates a new JexlException.
    078     *
    079     * @param node  the node causing the error
    080     * @param msg   the error message
    081     * @param cause the exception causing the error
    082     * @param trace whether this exception has a stacktrace and can <em>not</em> be suppressed
    083     */
    084    protected JexlException(final JexlNode node, final String msg, final Throwable cause, boolean trace) {
    085        super(msg != null ? msg : "", unwrap(cause), !trace, trace);
    086        if (node != null) {
    087            mark = node;
    088            info = node.jexlInfo();
    089        } else {
    090            mark = null;
    091            info = null;
    092        }
    093    }
    094
    095    /**
    096     * Creates a new JexlException.
    097     *
    098     * @param jinfo the debugging information associated
    099     * @param msg   the error message
    100     * @param cause the exception causing the error
    101     */
    102    public JexlException(final JexlInfo jinfo, final String msg, final Throwable cause) {
    103        super(msg != null ? msg : "", unwrap(cause));
    104        mark = null;
    105        info = jinfo;
    106    }
    107
    108    /**
    109     * Gets the specific information for this exception.
    110     *
    111     * @return the information
    112     */
    113    public JexlInfo getInfo() {
    114        return detailedInfo(mark, info);
    115    }
    116
    117    /**
    118     * Creates a string builder pre-filled with common error information (if possible).
    119     *
    120     * @param node the node
    121     * @return a string builder
    122     */
    123    private static StringBuilder errorAt(final JexlNode node) {
    124        final JexlInfo info = node != null? detailedInfo(node, node.jexlInfo()) : null;
    125        final StringBuilder msg = new StringBuilder();
    126        if (info != null) {
    127            msg.append(info.toString());
    128        } else {
    129            msg.append("?:");
    130        }
    131        msg.append(' ');
    132        return msg;
    133    }
    134
    135    /**
    136     * Gets the most specific information attached to a node.
    137     *
    138     * @param node the node
    139     * @param info the information
    140     * @return the information or null
    141     * @deprecated 3.2
    142     */
    143    @Deprecated
    144    public static JexlInfo getInfo(final JexlNode node, final JexlInfo info) {
    145        return detailedInfo(node, info);
    146    }
    147
    148    /**
    149     * Gets the most specific information attached to a node.
    150     *
    151     * @param node the node
    152     * @param info the information
    153     * @return the information or null
    154     */
    155    private static JexlInfo detailedInfo(final JexlNode node, final JexlInfo info) {
    156        if (info != null && node != null) {
    157            final Debugger dbg = new Debugger();
    158            if (dbg.debug(node)) {
    159                return new JexlInfo(info) {
    160                    @Override
    161                    public JexlInfo.Detail getDetail() {
    162                        return dbg;
    163                    }
    164                };
    165            }
    166        }
    167        return info;
    168    }
    169
    170    /**
    171     * Cleans a JexlException from any org.apache.commons.jexl3.internal stack trace element.
    172     *
    173     * @return this exception
    174     */
    175    public JexlException clean() {
    176        return clean(this);
    177    }
    178
    179    /**
    180     * Cleans a Throwable from any org.apache.commons.jexl3.internal stack trace element.
    181     *
    182     * @param <X>    the throwable type
    183     * @param xthrow the thowable
    184     * @return the throwable
    185     */
    186    private static <X extends Throwable> X clean(final X xthrow) {
    187        if (xthrow != null) {
    188            final List<StackTraceElement> stackJexl = new ArrayList<>();
    189            for (final StackTraceElement se : xthrow.getStackTrace()) {
    190                final String className = se.getClassName();
    191                if (!className.startsWith("org.apache.commons.jexl3.internal")
    192                        && !className.startsWith("org.apache.commons.jexl3.parser")) {
    193                    stackJexl.add(se);
    194                }
    195            }
    196            xthrow.setStackTrace(stackJexl.toArray(new StackTraceElement[stackJexl.size()]));
    197        }
    198        return xthrow;
    199    }
    200
    201    /**
    202     * Unwraps the cause of a throwable due to reflection.
    203     *
    204     * @param xthrow the throwable
    205     * @return the cause
    206     */
    207    private static Throwable unwrap(final Throwable xthrow) {
    208        if (xthrow instanceof TryFailed
    209            || xthrow instanceof InvocationTargetException
    210            || xthrow instanceof UndeclaredThrowableException) {
    211            return xthrow.getCause();
    212        }
    213        return xthrow;
    214    }
    215
    216    /**
    217     * Merge the node info and the cause info to obtain best possible location.
    218     *
    219     * @param info  the node
    220     * @param cause the cause
    221     * @return the info to use
    222     */
    223    private static JexlInfo merge(final JexlInfo info, final JavaccError cause) {
    224        if (cause == null || cause.getLine() < 0) {
    225            return info;
    226        }
    227        if (info == null) {
    228            return new JexlInfo("", cause.getLine(), cause.getColumn());
    229        }
    230        return new JexlInfo(info.getName(), cause.getLine(), cause.getColumn());
    231    }
    232
    233    /**
    234     * Accesses detailed message.
    235     *
    236     * @return the message
    237     */
    238    protected String detailedMessage() {
    239        Class<? extends JexlException> clazz = getClass();
    240        String name = clazz == JexlException.class? "JEXL" : clazz.getSimpleName().toLowerCase();
    241        return name + " error : " + getDetail();
    242    }
    243
    244    /**
    245     * @return this exception specific detail
    246     * @since 3.2
    247     */
    248    public final String getDetail() {
    249        return super.getMessage();
    250    }
    251
    252    /**
    253     * Formats an error message from the parser.
    254     *
    255     * @param prefix the prefix to the message
    256     * @param expr   the expression in error
    257     * @return the formatted message
    258     */
    259    protected String parserError(final String prefix, final String expr) {
    260        final int length = expr.length();
    261        if (length < MAX_EXCHARLOC) {
    262            return prefix + " error in '" + expr + "'";
    263        }
    264        final int me = MAX_EXCHARLOC / 2;
    265        int begin = info.getColumn() - me;
    266        if (begin < 0 || length < me) {
    267            begin = 0;
    268        } else if (begin > length) {
    269            begin = me;
    270        }
    271        int end = begin + MAX_EXCHARLOC;
    272        if (end > length) {
    273            end = length;
    274        }
    275        return prefix + " error near '... "
    276                + expr.substring(begin, end) + " ...'";
    277    }
    278
    279    /**
    280     * Pleasing checkstyle.
    281     * @return the info
    282     */
    283    protected JexlInfo info() {
    284        return info;
    285    }
    286
    287    /**
    288     * Thrown when tokenization fails.
    289     *
    290     * @since 3.0
    291     */
    292    public static class Tokenization extends JexlException {
    293        private static final long serialVersionUID = 20210606123901L;
    294        /**
    295         * Creates a new Tokenization exception instance.
    296         * @param info  the location info
    297         * @param cause the javacc cause
    298         */
    299        public Tokenization(final JexlInfo info, final TokenMgrException cause) {
    300            super(merge(info, cause), Objects.requireNonNull(cause).getAfter(), null);
    301        }
    302
    303        @Override
    304        protected String detailedMessage() {
    305            return parserError("tokenization", getDetail());
    306        }
    307    }
    308
    309    /**
    310     * Thrown when parsing fails.
    311     *
    312     * @since 3.0
    313     */
    314    public static class Parsing extends JexlException {
    315        private static final long serialVersionUID = 20210606123902L;
    316        /**
    317         * Creates a new Parsing exception instance.
    318         *
    319         * @param info  the location information
    320         * @param cause the javacc cause
    321         */
    322        public Parsing(final JexlInfo info, final ParseException cause) {
    323            super(merge(info, cause), Objects.requireNonNull(cause).getAfter(), null);
    324        }
    325
    326        /**
    327         * Creates a new Parsing exception instance.
    328         *
    329         * @param info the location information
    330         * @param msg  the message
    331         */
    332        public Parsing(final JexlInfo info, final String msg) {
    333            super(info, msg, null);
    334        }
    335
    336        @Override
    337        protected String detailedMessage() {
    338            return parserError("parsing", getDetail());
    339        }
    340    }
    341
    342    /**
    343     * Thrown when parsing fails due to an ambiguous statement.
    344     *
    345     * @since 3.0
    346     */
    347    public static class Ambiguous extends Parsing {
    348        private static final long serialVersionUID = 20210606123903L;
    349        /** The mark at which ambiguity might stop and recover. */
    350        private final transient JexlInfo recover;
    351        /**
    352         * Creates a new Ambiguous statement exception instance.
    353         * @param info  the location information
    354         * @param expr  the source expression line
    355         */
    356        public Ambiguous(final JexlInfo info, final String expr) {
    357           this(info, null, expr);
    358        }
    359
    360        /**
    361         * Creates a new Ambiguous statement exception instance.
    362         * @param begin  the start location information
    363         * @param end the end location information
    364         * @param expr  the source expression line
    365         */
    366        public Ambiguous(final JexlInfo begin, final JexlInfo end, final String expr) {
    367            super(begin, expr);
    368            recover = end;
    369        }
    370
    371        @Override
    372        protected String detailedMessage() {
    373            return parserError("ambiguous statement", getDetail());
    374        }
    375
    376        /**
    377         * Tries to remove this ambiguity in the source.
    378         * @param src the source that triggered this exception
    379         * @return the source with the ambiguous statement removed
    380         *         or null if no recovery was possible
    381         */
    382        public String tryCleanSource(final String src) {
    383            final JexlInfo ji = info();
    384            return ji == null || recover == null
    385                  ? src
    386                  : sliceSource(src, ji.getLine(), ji.getColumn(), recover.getLine(), recover.getColumn());
    387        }
    388    }
    389
    390    /**
    391     * Removes a slice from a source.
    392     * @param src the source
    393     * @param froml the begin line
    394     * @param fromc the begin column
    395     * @param tol the to line
    396     * @param toc the to column
    397     * @return the source with the (begin) to (to) zone removed
    398     */
    399    public static String sliceSource(final String src, final int froml, final int fromc, final int tol, final int toc) {
    400        final BufferedReader reader = new BufferedReader(new StringReader(src));
    401        final StringBuilder buffer = new StringBuilder();
    402        String line;
    403        int cl = 1;
    404        try {
    405            while ((line = reader.readLine()) != null) {
    406                if (cl < froml || cl > tol) {
    407                    buffer.append(line).append('\n');
    408                } else {
    409                    if (cl == froml) {
    410                        buffer.append(line, 0, fromc - 1);
    411                    }
    412                    if (cl == tol) {
    413                        buffer.append(line.substring(toc + 1));
    414                    }
    415                } // else ignore line
    416                cl += 1;
    417            }
    418        } catch (final IOException xignore) {
    419            //damn the checked exceptions :-)
    420        }
    421        return buffer.toString();
    422    }
    423
    424    /**
    425     * Thrown when reaching stack-overflow.
    426     *
    427     * @since 3.2
    428     */
    429    public static class StackOverflow extends JexlException {
    430        private static final long serialVersionUID = 20210606123904L;
    431        /**
    432         * Creates a new stack overflow exception instance.
    433         *
    434         * @param info  the location information
    435         * @param name  the unknown method
    436         * @param cause the exception causing the error
    437         */
    438        public StackOverflow(final JexlInfo info, final String name, final Throwable cause) {
    439            super(info, name, cause);
    440        }
    441
    442        @Override
    443        protected String detailedMessage() {
    444            return "stack overflow " + getDetail();
    445        }
    446    }
    447
    448    /**
    449     * Thrown when parsing fails due to an invalid assigment.
    450     *
    451     * @since 3.0
    452     */
    453    public static class Assignment extends Parsing {
    454        private static final long serialVersionUID = 20210606123905L;
    455        /**
    456         * Creates a new Assignment statement exception instance.
    457         *
    458         * @param info  the location information
    459         * @param expr  the source expression line
    460         */
    461        public Assignment(final JexlInfo info, final String expr) {
    462            super(info, expr);
    463        }
    464
    465        @Override
    466        protected String detailedMessage() {
    467            return parserError("assignment", getDetail());
    468        }
    469    }
    470
    471    /**
    472     * Thrown when parsing fails due to a disallowed feature.
    473     *
    474     * @since 3.2
    475     */
    476    public static class Feature extends Parsing {
    477        private static final long serialVersionUID = 20210606123906L;
    478        /** The feature code. */
    479        private final int code;
    480        /**
    481         * Creates a new Ambiguous statement exception instance.
    482         * @param info  the location information
    483         * @param feature the feature code
    484         * @param expr  the source expression line
    485         */
    486        public Feature(final JexlInfo info, final int feature, final String expr) {
    487            super(info, expr);
    488            this.code = feature;
    489        }
    490
    491        @Override
    492        protected String detailedMessage() {
    493            return parserError(JexlFeatures.stringify(code), getDetail());
    494        }
    495    }
    496
    497    /** Used 3 times. */
    498    private static final String VARQUOTE = "variable '";
    499
    500    /**
    501     * The various type of variable issues.
    502     */
    503    public enum VariableIssue {
    504        /** The variable is undefined. */
    505        UNDEFINED,
    506        /** The variable is already declared. */
    507        REDEFINED,
    508        /** The variable has a null value. */
    509        NULLVALUE;
    510
    511        /**
    512         * Stringifies the variable issue.
    513         * @param var the variable name
    514         * @return the issue message
    515         */
    516        public String message(final String var) {
    517            switch(this) {
    518                case NULLVALUE : return VARQUOTE + var + "' is null";
    519                case REDEFINED : return VARQUOTE + var + "' is already defined";
    520                case UNDEFINED :
    521                default: return VARQUOTE + var + "' is undefined";
    522            }
    523        }
    524    }
    525
    526    /**
    527     * Thrown when a variable is unknown.
    528     *
    529     * @since 3.0
    530     */
    531    public static class Variable extends JexlException {
    532        private static final long serialVersionUID = 20210606123907L;
    533        /**
    534         * Undefined variable flag.
    535         */
    536        private final VariableIssue issue;
    537
    538        /**
    539         * Creates a new Variable exception instance.
    540         *
    541         * @param node the offending ASTnode
    542         * @param var  the unknown variable
    543         * @param vi   the variable issue
    544         */
    545        public Variable(final JexlNode node, final String var, final VariableIssue vi) {
    546            super(node, var, null);
    547            issue = vi;
    548        }
    549
    550        /**
    551         * Creates a new Variable exception instance.
    552         *
    553         * @param node the offending ASTnode
    554         * @param var  the unknown variable
    555         * @param undef whether the variable is undefined or evaluated as null
    556         */
    557        public Variable(final JexlNode node, final String var, final boolean undef) {
    558            this(node, var,  undef ? VariableIssue.UNDEFINED : VariableIssue.NULLVALUE);
    559        }
    560
    561        /**
    562         * Whether the variable causing an error is undefined or evaluated as null.
    563         *
    564         * @return true if undefined, false otherwise
    565         */
    566        public boolean isUndefined() {
    567            return issue == VariableIssue.UNDEFINED;
    568        }
    569
    570        /**
    571         * @return the variable name
    572         */
    573        public String getVariable() {
    574            return getDetail();
    575        }
    576
    577        @Override
    578        protected String detailedMessage() {
    579            return issue.message(getVariable());
    580        }
    581    }
    582
    583    /**
    584     * Generates a message for a variable error.
    585     *
    586     * @param node the node where the error occurred
    587     * @param variable the variable
    588     * @param undef whether the variable is null or undefined
    589     * @return the error message
    590     * @deprecated 3.2
    591     */
    592    @Deprecated
    593    public static String variableError(final JexlNode node, final String variable, final boolean undef) {
    594        return variableError(node, variable, undef? VariableIssue.UNDEFINED : VariableIssue.NULLVALUE);
    595    }
    596
    597    /**
    598     * Generates a message for a variable error.
    599     *
    600     * @param node the node where the error occurred
    601     * @param variable the variable
    602     * @param issue  the variable kind of issue
    603     * @return the error message
    604     */
    605    public static String variableError(final JexlNode node, final String variable, final VariableIssue issue) {
    606        final StringBuilder msg = errorAt(node);
    607        msg.append(issue.message(variable));
    608        return msg.toString();
    609    }
    610
    611    /**
    612     * Thrown when a property is unknown.
    613     *
    614     * @since 3.0
    615     */
    616    public static class Property extends JexlException {
    617        private static final long serialVersionUID = 20210606123908L;
    618        /**
    619         * Undefined variable flag.
    620         */
    621        private final boolean undefined;
    622
    623        /**
    624         * Creates a new Property exception instance.
    625         *
    626         * @param node the offending ASTnode
    627         * @param pty  the unknown property
    628         * @deprecated 3.2
    629         */
    630        @Deprecated
    631        public Property(final JexlNode node, final String pty) {
    632            this(node, pty, true, null);
    633        }
    634        /**
    635         * Creates a new Property exception instance.
    636         *
    637         * @param node the offending ASTnode
    638         * @param pty  the unknown property
    639         * @param cause the exception causing the error
    640         * @deprecated 3.2
    641         */
    642        @Deprecated
    643        public Property(final JexlNode node, final String pty, final Throwable cause) {
    644            this(node, pty, true, cause);
    645        }
    646
    647        /**
    648         * Creates a new Property exception instance.
    649         *
    650         * @param node the offending ASTnode
    651         * @param pty  the unknown property
    652         * @param undef whether the variable is null or undefined
    653         * @param cause the exception causing the error
    654         */
    655        public Property(final JexlNode node, final String pty, final boolean undef, final Throwable cause) {
    656            super(node, pty, cause);
    657            undefined = undef;
    658        }
    659
    660        /**
    661         * Whether the variable causing an error is undefined or evaluated as null.
    662         *
    663         * @return true if undefined, false otherwise
    664         */
    665        public boolean isUndefined() {
    666            return undefined;
    667        }
    668
    669        /**
    670         * @return the property name
    671         */
    672        public String getProperty() {
    673            return getDetail();
    674        }
    675
    676        @Override
    677        protected String detailedMessage() {
    678            return (undefined? "undefined" : "null value") + " property '" + getProperty() + "'";
    679        }
    680    }
    681
    682    /**
    683     * Generates a message for an unsolvable property error.
    684     *
    685     * @param node the node where the error occurred
    686     * @param pty the property
    687     * @param undef whether the property is null or undefined
    688     * @return the error message
    689     */
    690    public static String propertyError(final JexlNode node, final String pty, final boolean undef) {
    691        final StringBuilder msg = errorAt(node);
    692        if (undef) {
    693            msg.append("unsolvable");
    694        } else {
    695            msg.append("null value");
    696        }
    697        msg.append(" property '");
    698        msg.append(pty);
    699        msg.append('\'');
    700        return msg.toString();
    701    }
    702
    703    /**
    704     * Generates a message for an unsolvable property error.
    705     *
    706     * @param node the node where the error occurred
    707     * @param var the variable
    708     * @return the error message
    709     * @deprecated 3.2
    710     */
    711    @Deprecated
    712    public static String propertyError(final JexlNode node, final String var) {
    713        return propertyError(node, var, true);
    714    }
    715
    716    /**
    717     * Thrown when a method or ctor is unknown, ambiguous or inaccessible.
    718     *
    719     * @since 3.0
    720     */
    721    public static class Method extends JexlException {
    722        private static final long serialVersionUID = 20210606123909L;
    723        /**
    724         * Creates a new Method exception instance.
    725         *
    726         * @param node  the offending ASTnode
    727         * @param name  the method name
    728         * @deprecated as of 3.2, use call with method arguments
    729         */
    730        @Deprecated
    731        public Method(final JexlNode node, final String name) {
    732            this(node, name, null);
    733        }
    734
    735        /**
    736         * Creates a new Method exception instance.
    737         *
    738         * @param info  the location information
    739         * @param name  the unknown method
    740         * @param cause the exception causing the error
    741         * @deprecated as of 3.2, use call with method arguments
    742         */
    743        @Deprecated
    744        public Method(final JexlInfo info, final String name, final Throwable cause) {
    745            this(info, name, null, cause);
    746        }
    747
    748        /**
    749         * Creates a new Method exception instance.
    750         *
    751         * @param node  the offending ASTnode
    752         * @param name  the method name
    753         * @param args  the method arguments
    754         * @since 3.2
    755         */
    756        public Method(final JexlNode node, final String name, final Object[] args) {
    757            super(node, methodSignature(name, args));
    758        }
    759
    760        /**
    761         * Creates a new Method exception instance.
    762         *
    763         * @param info  the location information
    764         * @param name  the method name
    765         * @param args  the method arguments
    766         * @since 3.2
    767         */
    768        public Method(final JexlInfo info, final String name, final Object[] args) {
    769            this(info, name, args, null);
    770        }
    771
    772
    773        /**
    774         * Creates a new Method exception instance.
    775         *
    776         * @param info  the location information
    777         * @param name  the method name
    778         * @param cause the exception causing the error
    779         * @param args  the method arguments
    780         * @since 3.2
    781         */
    782        public Method(final JexlInfo info, final String name, final Object[] args, final Throwable cause) {
    783            super(info, methodSignature(name, args), cause);
    784        }
    785
    786        /**
    787         * @return the method name
    788         */
    789        public String getMethod() {
    790            final String signature = getMethodSignature();
    791            final int lparen = signature.indexOf('(');
    792            return lparen > 0? signature.substring(0, lparen) : signature;
    793        }
    794
    795        /**
    796         * @return the method signature
    797         * @since 3.2
    798         */
    799        public String getMethodSignature() {
    800            return getDetail();
    801        }
    802
    803        @Override
    804        protected String detailedMessage() {
    805            return "unsolvable function/method '" + getMethodSignature() + "'";
    806        }
    807    }
    808
    809    /**
    810     * Creates a signed-name for a given method name and arguments.
    811     * @param name the method name
    812     * @param args the method arguments
    813     * @return a suitable signed name
    814     */
    815    private static String methodSignature(final String name, final Object[] args) {
    816        if (args != null && args.length > 0) {
    817            final StringBuilder strb = new StringBuilder(name);
    818            strb.append('(');
    819            for (int a = 0; a < args.length; ++a) {
    820                if (a > 0) {
    821                    strb.append(", ");
    822                }
    823                final Class<?> clazz = args[a] == null ? Object.class : args[a].getClass();
    824                strb.append(clazz.getSimpleName());
    825            }
    826            strb.append(')');
    827            return strb.toString();
    828        }
    829        return name;
    830    }
    831
    832    /**
    833     * Generates a message for a unsolvable method error.
    834     *
    835     * @param node the node where the error occurred
    836     * @param method the method name
    837     * @return the error message
    838     * @deprecated 3.2
    839     */
    840    @Deprecated
    841    public static String methodError(final JexlNode node, final String method) {
    842        return methodError(node, method, null);
    843    }
    844
    845    /**
    846     * Generates a message for a unsolvable method error.
    847     *
    848     * @param node the node where the error occurred
    849     * @param method the method name
    850     * @param args the method arguments
    851     * @return the error message
    852     */
    853    public static String methodError(final JexlNode node, final String method, final Object[] args) {
    854        final StringBuilder msg = errorAt(node);
    855        msg.append("unsolvable function/method '");
    856        msg.append(methodSignature(method, args));
    857        msg.append('\'');
    858        return msg.toString();
    859    }
    860
    861    /**
    862     * Thrown when an operator fails.
    863     *
    864     * @since 3.0
    865     */
    866    public static class Operator extends JexlException {
    867        private static final long serialVersionUID = 20210606124100L;
    868        /**
    869         * Creates a new Operator exception instance.
    870         *
    871         * @param node  the location information
    872         * @param symbol  the operator name
    873         * @param cause the exception causing the error
    874         */
    875        public Operator(final JexlNode node, final String symbol, final Throwable cause) {
    876            super(node, symbol, cause);
    877        }
    878
    879        /**
    880         * @return the method name
    881         */
    882        public String getSymbol() {
    883            return getDetail();
    884        }
    885
    886        @Override
    887        protected String detailedMessage() {
    888            return "error calling operator '" + getSymbol() + "'";
    889        }
    890    }
    891
    892    /**
    893     * Generates a message for an operator error.
    894     *
    895     * @param node the node where the error occurred
    896     * @param symbol the operator name
    897     * @return the error message
    898     */
    899    public static String operatorError(final JexlNode node, final String symbol) {
    900        final StringBuilder msg = errorAt(node);
    901        msg.append("error calling operator '");
    902        msg.append(symbol);
    903        msg.append('\'');
    904        return msg.toString();
    905    }
    906
    907    /**
    908     * Thrown when an annotation handler throws an exception.
    909     *
    910     * @since 3.1
    911     */
    912    public static class Annotation extends JexlException {
    913        private static final long serialVersionUID = 20210606124101L;
    914        /**
    915         * Creates a new Annotation exception instance.
    916         *
    917         * @param node  the annotated statement node
    918         * @param name  the annotation name
    919         * @param cause the exception causing the error
    920         */
    921        public Annotation(final JexlNode node, final String name, final Throwable cause) {
    922            super(node, name, cause);
    923        }
    924
    925        /**
    926         * @return the annotation name
    927         */
    928        public String getAnnotation() {
    929            return getDetail();
    930        }
    931
    932        @Override
    933        protected String detailedMessage() {
    934            return "error processing annotation '" + getAnnotation() + "'";
    935        }
    936    }
    937
    938    /**
    939     * Generates a message for an annotation error.
    940     *
    941     * @param node the node where the error occurred
    942     * @param annotation the annotation name
    943     * @return the error message
    944     * @since 3.1
    945     */
    946    public static String annotationError(final JexlNode node, final String annotation) {
    947        final StringBuilder msg = errorAt(node);
    948        msg.append("error processing annotation '");
    949        msg.append(annotation);
    950        msg.append('\'');
    951        return msg.toString();
    952    }
    953
    954    /**
    955     * Thrown to return a value.
    956     *
    957     * @since 3.0
    958     */
    959    public static class Return extends JexlException {
    960        private static final long serialVersionUID = 20210606124102L;
    961
    962        /** The returned value. */
    963        private final transient Object result;
    964
    965        /**
    966         * Creates a new instance of Return.
    967         *
    968         * @param node  the return node
    969         * @param msg   the message
    970         * @param value the returned value
    971         */
    972        public Return(final JexlNode node, final String msg, final Object value) {
    973            super(node, msg, null, false);
    974            this.result = value;
    975        }
    976
    977        /**
    978         * @return the returned value
    979         */
    980        public Object getValue() {
    981            return result;
    982        }
    983    }
    984
    985    /**
    986     * Thrown to cancel a script execution.
    987     *
    988     * @since 3.0
    989     */
    990    public static class Cancel extends JexlException {
    991        /**
    992         * Creates a new instance of Cancel.
    993         *
    994         * @param node the node where the interruption was detected
    995         */
    996        public Cancel(final JexlNode node) {
    997            super(node, "execution cancelled", null);
    998        }
    999    }
    1000
    1001    /**
    1002     * Thrown to break a loop.
    1003     *
    1004     * @since 3.0
    1005     */
    1006    public static class Break extends JexlException {
    1007        private static final long serialVersionUID = 20210606124103L;
    1008        /**
    1009         * Creates a new instance of Break.
    1010         *
    1011         * @param node the break
    1012         */
    1013        public Break(final JexlNode node) {
    1014            super(node, "break loop", null, false);
    1015        }
    1016    }
    1017
    1018    /**
    1019     * Thrown to continue a loop.
    1020     *
    1021     * @since 3.0
    1022     */
    1023    public static class Continue extends JexlException {
    1024        private static final long serialVersionUID = 20210606124104L;
    1025        /**
    1026         * Creates a new instance of Continue.
    1027         *
    1028         * @param node the continue
    1029         */
    1030        public Continue(final JexlNode node) {
    1031            super(node, "continue loop", null, false);
    1032        }
    1033    }
    1034
    1035    /**
    1036     * Thrown when method/ctor invocation fails.
    1037     * <p>These wrap InvocationTargetException as runtime exception
    1038     * allowing to go through without signature modifications.
    1039     * @since 3.2
    1040     */
    1041    public static class TryFailed extends JexlException {
    1042        private static final long serialVersionUID = 20210606124105L;
    1043        /**
    1044         * Creates a new instance.
    1045         * @param xany the original invocation target exception
    1046         */
    1047        private TryFailed(final InvocationTargetException xany) {
    1048            super((JexlInfo) null, "tryFailed", xany.getCause());
    1049        }
    1050    }
    1051
    1052    /**
    1053     * Wrap an invocation exception.
    1054     * <p>Return the cause if it is already a JexlException.
    1055     * @param xinvoke the invocation exception
    1056     * @return a JexlException
    1057     */
    1058    public static JexlException tryFailed(final InvocationTargetException xinvoke) {
    1059        final Throwable cause = xinvoke.getCause();
    1060        return cause instanceof JexlException
    1061                ? (JexlException) cause
    1062                : new JexlException.TryFailed(xinvoke); // fail
    1063    }
    1064
    1065
    1066    /**
    1067     * Detailed info message about this error.
    1068     * Format is "debug![begin,end]: string \n msg" where:
    1069     *
    1070     * - debug is the debugging information if it exists (@link JexlEngine.setDebug)
    1071     * - begin, end are character offsets in the string for the precise location of the error
    1072     * - string is the string representation of the offending expression
    1073     * - msg is the actual explanation message for this error
    1074     *
    1075     * @return this error as a string
    1076     */
    1077    @Override
    1078    public String getMessage() {
    1079        final StringBuilder msg = new StringBuilder();
    1080        if (info != null) {
    1081            msg.append(info.toString());
    1082        } else {
    1083            msg.append("?:");
    1084        }
    1085        msg.append(' ');
    1086        msg.append(detailedMessage());
    1087        final Throwable cause = getCause();
    1088        if (cause instanceof JexlArithmetic.NullOperand) {
    1089            msg.append(" caused by null operand");
    1090        }
    1091        return msg.toString();
    1092    }
    1093}