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