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