    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, final 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     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     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     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[0]));
    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    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 the best possible location.
    218     *
    219     * @param info  the node
    220     * @param cause the cause
    221     * @return the info to use
    222     */
    223    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        final Class<? extends JexlException> clazz = getClass();
    240        final 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 beginning line
    394     * @param fromc the beginning column
    395     * @param tol the ending line
    396     * @param toc the ending 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 assignment.
    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        /**
    636         * Creates a new Property exception instance.
    637         *
    638         * @param node the offending ASTnode
    639         * @param pty  the unknown property
    640         * @param cause the exception causing the error
    641         * @deprecated 3.2
    642         */
    643        @Deprecated
    644        public Property(final JexlNode node, final String pty, final Throwable cause) {
    645            this(node, pty, true, cause);
    646        }
    647
    648        /**
    649         * Creates a new Property exception instance.
    650         *
    651         * @param node the offending ASTnode
    652         * @param pty  the unknown property
    653         * @param undef whether the variable is null or undefined
    654         * @param cause the exception causing the error
    655         */
    656        public Property(final JexlNode node, final String pty, final boolean undef, final Throwable cause) {
    657            super(node, pty, cause);
    658            undefined = undef;
    659        }
    660
    661        /**
    662         * Whether the variable causing an error is undefined or evaluated as null.
    663         *
    664         * @return true if undefined, false otherwise
    665         */
    666        public boolean isUndefined() {
    667            return undefined;
    668        }
    669
    670        /**
    671         * @return the property name
    672         */
    673        public String getProperty() {
    674            return getDetail();
    675        }
    676
    677        @Override
    678        protected String detailedMessage() {
    679            return (undefined? "undefined" : "null value") + " property '" + getProperty() + "'";
    680        }
    681    }
    682
    683    /**
    684     * Generates a message for an unsolvable property error.
    685     *
    686     * @param node the node where the error occurred
    687     * @param pty the property
    688     * @param undef whether the property is null or undefined
    689     * @return the error message
    690     */
    691    public static String propertyError(final JexlNode node, final String pty, final boolean undef) {
    692        final StringBuilder msg = errorAt(node);
    693        if (undef) {
    694            msg.append("unsolvable");
    695        } else {
    696            msg.append("null value");
    697        }
    698        msg.append(" property '");
    699        msg.append(pty);
    700        msg.append('\'');
    701        return msg.toString();
    702    }
    703
    704    /**
    705     * Generates a message for an unsolvable property error.
    706     *
    707     * @param node the node where the error occurred
    708     * @param var the variable
    709     * @return the error message
    710     * @deprecated 3.2
    711     */
    712    @Deprecated
    713    public static String propertyError(final JexlNode node, final String var) {
    714        return propertyError(node, var, true);
    715    }
    716
    717    /**
    718     * Thrown when a method or ctor is unknown, ambiguous or inaccessible.
    719     *
    720     * @since 3.0
    721     */
    722    public static class Method extends JexlException {
    723        private static final long serialVersionUID = 20210606123909L;
    724        /**
    725         * Creates a new Method exception instance.
    726         *
    727         * @param node  the offending ASTnode
    728         * @param name  the method name
    729         * @deprecated as of 3.2, use call with method arguments
    730         */
    731        @Deprecated
    732        public Method(final JexlNode node, final String name) {
    733            this(node, name, null);
    734        }
    735
    736        /**
    737         * Creates a new Method exception instance.
    738         *
    739         * @param info  the location information
    740         * @param name  the unknown method
    741         * @param cause the exception causing the error
    742         * @deprecated as of 3.2, use call with method arguments
    743         */
    744        @Deprecated
    745        public Method(final JexlInfo info, final String name, final Throwable cause) {
    746            this(info, name, null, cause);
    747        }
    748
    749        /**
    750         * Creates a new Method exception instance.
    751         *
    752         * @param node  the offending ASTnode
    753         * @param name  the method name
    754         * @param args  the method arguments
    755         * @since 3.2
    756         */
    757        public Method(final JexlNode node, final String name, final Object[] args) {
    758            super(node, methodSignature(name, args));
    759        }
    760
    761        /**
    762         * Creates a new Method exception instance.
    763         *
    764         * @param info  the location information
    765         * @param name  the method name
    766         * @param args  the method arguments
    767         * @since 3.2
    768         */
    769        public Method(final JexlInfo info, final String name, final Object[] args) {
    770            this(info, name, args, null);
    771        }
    772
    773
    774        /**
    775         * Creates a new Method exception instance.
    776         *
    777         * @param info  the location information
    778         * @param name  the method name
    779         * @param cause the exception causing the error
    780         * @param args  the method arguments
    781         * @since 3.2
    782         */
    783        public Method(final JexlInfo info, final String name, final Object[] args, final Throwable cause) {
    784            super(info, methodSignature(name, args), cause);
    785        }
    786
    787        /**
    788         * @return the method name
    789         */
    790        public String getMethod() {
    791            final String signature = getMethodSignature();
    792            final int lparen = signature.indexOf('(');
    793            return lparen > 0? signature.substring(0, lparen) : signature;
    794        }
    795
    796        /**
    797         * @return the method signature
    798         * @since 3.2
    799         */
    800        public String getMethodSignature() {
    801            return getDetail();
    802        }
    803
    804        @Override
    805        protected String detailedMessage() {
    806            return "unsolvable function/method '" + getMethodSignature() + "'";
    807        }
    808    }
    809
    810    /**
    811     * Creates a signed-name for a given method name and arguments.
    812     * @param name the method name
    813     * @param args the method arguments
    814     * @return a suitable signed name
    815     */
    816     static String methodSignature(final String name, final Object[] args) {
    817        if (args != null && args.length > 0) {
    818            final StringBuilder strb = new StringBuilder(name);
    819            strb.append('(');
    820            for (int a = 0; a < args.length; ++a) {
    821                if (a > 0) {
    822                    strb.append(", ");
    823                }
    824                final Class<?> clazz = args[a] == null ? Object.class : args[a].getClass();
    825                strb.append(clazz.getSimpleName());
    826            }
    827            strb.append(')');
    828            return strb.toString();
    829        }
    830        return name;
    831    }
    832
    833    /**
    834     * Generates a message for a unsolvable method error.
    835     *
    836     * @param node the node where the error occurred
    837     * @param method the method name
    838     * @return the error message
    839     * @deprecated 3.2
    840     */
    841    @Deprecated
    842    public static String methodError(final JexlNode node, final String method) {
    843        return methodError(node, method, null);
    844    }
    845
    846    /**
    847     * Generates a message for a unsolvable method error.
    848     *
    849     * @param node the node where the error occurred
    850     * @param method the method name
    851     * @param args the method arguments
    852     * @return the error message
    853     */
    854    public static String methodError(final JexlNode node, final String method, final Object[] args) {
    855        final StringBuilder msg = errorAt(node);
    856        msg.append("unsolvable function/method '");
    857        msg.append(methodSignature(method, args));
    858        msg.append('\'');
    859        return msg.toString();
    860    }
    861
    862    /**
    863     * Thrown when an operator fails.
    864     *
    865     * @since 3.0
    866     */
    867    public static class Operator extends JexlException {
    868        private static final long serialVersionUID = 20210606124100L;
    869        /**
    870         * Creates a new Operator exception instance.
    871         *
    872         * @param node  the location information
    873         * @param symbol  the operator name
    874         * @param cause the exception causing the error
    875         */
    876        public Operator(final JexlNode node, final String symbol, final Throwable cause) {
    877            super(node, symbol, cause);
    878        }
    879
    880        /**
    881         * @return the method name
    882         */
    883        public String getSymbol() {
    884            return getDetail();
    885        }
    886
    887        @Override
    888        protected String detailedMessage() {
    889            return "error calling operator '" + getSymbol() + "'";
    890        }
    891    }
    892
    893    /**
    894     * Generates a message for an operator error.
    895     *
    896     * @param node the node where the error occurred
    897     * @param symbol the operator name
    898     * @return the error message
    899     */
    900    public static String operatorError(final JexlNode node, final String symbol) {
    901        final StringBuilder msg = errorAt(node);
    902        msg.append("error calling operator '");
    903        msg.append(symbol);
    904        msg.append('\'');
    905        return msg.toString();
    906    }
    907
    908    /**
    909     * Thrown when an annotation handler throws an exception.
    910     *
    911     * @since 3.1
    912     */
    913    public static class Annotation extends JexlException {
    914        private static final long serialVersionUID = 20210606124101L;
    915        /**
    916         * Creates a new Annotation exception instance.
    917         *
    918         * @param node  the annotated statement node
    919         * @param name  the annotation name
    920         * @param cause the exception causing the error
    921         */
    922        public Annotation(final JexlNode node, final String name, final Throwable cause) {
    923            super(node, name, cause);
    924        }
    925
    926        /**
    927         * @return the annotation name
    928         */
    929        public String getAnnotation() {
    930            return getDetail();
    931        }
    932
    933        @Override
    934        protected String detailedMessage() {
    935            return "error processing annotation '" + getAnnotation() + "'";
    936        }
    937    }
    938
    939    /**
    940     * Generates a message for an annotation error.
    941     *
    942     * @param node the node where the error occurred
    943     * @param annotation the annotation name
    944     * @return the error message
    945     * @since 3.1
    946     */
    947    public static String annotationError(final JexlNode node, final String annotation) {
    948        final StringBuilder msg = errorAt(node);
    949        msg.append("error processing annotation '");
    950        msg.append(annotation);
    951        msg.append('\'');
    952        return msg.toString();
    953    }
    954
    955    /**
    956     * Thrown to return a value.
    957     *
    958     * @since 3.0
    959     */
    960    public static class Return extends JexlException {
    961        private static final long serialVersionUID = 20210606124102L;
    962
    963        /** The returned value. */
    964        private final transient Object result;
    965
    966        /**
    967         * Creates a new instance of Return.
    968         *
    969         * @param node  the return node
    970         * @param msg   the message
    971         * @param value the returned value
    972         */
    973        public Return(final JexlNode node, final String msg, final Object value) {
    974            super(node, msg, null, false);
    975            this.result = value;
    976        }
    977
    978        /**
    979         * @return the returned value
    980         */
    981        public Object getValue() {
    982            return result;
    983        }
    984    }
    985
    986    /**
    987     * Thrown to cancel a script execution.
    988     *
    989     * @since 3.0
    990     */
    991    public static class Cancel extends JexlException {
    992        private static final long serialVersionUID = 7735706658499597964L;
    993        /**
    994         * Creates a new instance of Cancel.
    995         *
    996         * @param node the node where the interruption was detected
    997         */
    998        public Cancel(final JexlNode node) {
    999            super(node, "execution cancelled", null);
    1000        }
    1001    }
    1002
    1003    /**
    1004     * Thrown to break a loop.
    1005     *
    1006     * @since 3.0
    1007     */
    1008    public static class Break extends JexlException {
    1009        private static final long serialVersionUID = 20210606124103L;
    1010        /**
    1011         * Creates a new instance of Break.
    1012         *
    1013         * @param node the break
    1014         */
    1015        public Break(final JexlNode node) {
    1016            super(node, "break loop", null, false);
    1017        }
    1018    }
    1019
    1020    /**
    1021     * Thrown to continue a loop.
    1022     *
    1023     * @since 3.0
    1024     */
    1025    public static class Continue extends JexlException {
    1026        private static final long serialVersionUID = 20210606124104L;
    1027        /**
    1028         * Creates a new instance of Continue.
    1029         *
    1030         * @param node the continue-node
    1031         */
    1032        public Continue(final JexlNode node) {
    1033            super(node, "continue loop", null, false);
    1034        }
    1035    }
    1036
    1037    /**
    1038     * Thrown when method/ctor invocation fails.
    1039     * <p>These wrap InvocationTargetException as runtime exception
    1040     * allowing to go through without signature modifications.
    1041     * @since 3.2
    1042     */
    1043    public static class TryFailed extends JexlException {
    1044        private static final long serialVersionUID = 20210606124105L;
    1045        /**
    1046         * Creates a new instance.
    1047         * @param xany the original invocation target exception
    1048         */
    1049        private TryFailed(final InvocationTargetException xany) {
    1050            super((JexlInfo) null, "tryFailed", xany.getCause());
    1051        }
    1052    }
    1053
    1054    /**
    1055     * Wrap an invocation exception.
    1056     * <p>Return the cause if it is already a JexlException.
    1057     * @param xinvoke the invocation exception
    1058     * @return a JexlException
    1059     */
    1060    public static JexlException tryFailed(final InvocationTargetException xinvoke) {
    1061        final Throwable cause = xinvoke.getCause();
    1062        return cause instanceof JexlException
    1063                ? (JexlException) cause
    1064                : new JexlException.TryFailed(xinvoke); // fail
    1065    }
    1066
    1067
    1068    /**
    1069     * Detailed info message about this error.
    1070     * Format is "debug![begin,end]: string \n msg" where:
    1071     * - debug is the debugging information if it exists (@link JexlEngine.setDebug)
    1072     * - begin, end are character offsets in the string for the precise location of the error
    1073     * - string is the string representation of the offending expression
    1074     * - msg is the actual explanation message for this error
    1075     *
    1076     * @return this error as a string
    1077     */
    1078    @Override
    1079    public String getMessage() {
    1080        final StringBuilder msg = new StringBuilder();
    1081        if (info != null) {
    1082            msg.append(info.toString());
    1083        } else {
    1084            msg.append("?:");
    1085        }
    1086        msg.append(' ');
    1087        msg.append(detailedMessage());
    1088        final Throwable cause = getCause();
    1089        if (cause instanceof JexlArithmetic.NullOperand) {
    1090            msg.append(" caused by null operand");
    1091        }
    1092        return msg.toString();
    1093    }
    1094}