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