    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     * Enumerates types 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 cluttering stack trace elements.
    732     *
    733     * @param <X>    the throwable type
    734     * @param xthrow the throwable
    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 (startsWithAny(CLEAN_STOP, className)) {
    743                    break;
    744                }
    745                if (!startsWithAny(CLEAN_SKIP, className)) {
    746                    stackJexl.add(se);
    747                }
    748            }
    749            xthrow.setStackTrace(stackJexl.toArray(EMPTY_STACK_TRACE_ELEMENT_ARRAY));
    750        }
    751        return xthrow;
    752    }
    753
    754    /**
    755     * Checks whether a given name starts with any of the given prefixes.
    756     * @param prefixes the prefixes to check
    757     * @param name the name to check
    758     * @return true if the name starts with any of the prefixes, false otherwise
    759     */
    760    private static boolean startsWithAny(final String[] prefixes, final String name) {
    761        for (final String prefix : prefixes) {
    762            if (name.startsWith(prefix)) {
    763                return true;
    764            }
    765        }
    766        return false;
    767    }
    768
    769    /**
    770     * A static array of package names that will be skipped during
    771     * exception stack-trace cleansing.
    772     */
    773    private static final String[] CLEAN_SKIP = {
    774      "org.apache.commons.jexl3.internal",
    775      "org.apache.commons.jexl3.parser",
    776      "java.lang.reflect",
    777      "sun.reflect"
    778    };
    779
    780    /**
    781     * A static array of package names that will be considered as a stop point during
    782     * exception stack-trace cleansing.
    783     * <p>
    784     * All stacktrace elements occurring after one of these packages matches an element class name
    785     * will be discarded.
    786     * </p>
    787     */
    788    private static final String[] CLEAN_STOP = { "org.junit" };
    789
    790    /**
    791     * Gets the most specific information attached to a node.
    792     *
    793     * @param node the node
    794     * @param info the information
    795     * @return the information or null
    796     */
    797     static JexlInfo detailedInfo(final JexlNode node, final JexlInfo info) {
    798        if (info != null && node != null) {
    799            final Debugger dbg = new Debugger();
    800            if (dbg.debug(node)) {
    801                return new JexlInfo(info) {
    802                    @Override
    803                    public JexlInfo.Detail getDetail() {
    804                        return dbg;
    805                    }
    806                };
    807            }
    808        }
    809        return info;
    810    }
    811
    812    /**
    813     * Creates a string builder pre-filled with common error information (if possible).
    814     *
    815     * @param node the node
    816     * @return a string builder
    817     */
    818     static StringBuilder errorAt(final JexlNode node) {
    819        final JexlInfo info = node != null ? detailedInfo(node, node.jexlInfo()) : null;
    820        final StringBuilder msg = new StringBuilder();
    821        if (info != null) {
    822            msg.append(info);
    823        } else {
    824            msg.append("?:");
    825        }
    826        msg.append(' ');
    827        return msg;
    828    }
    829
    830    /**
    831     * Gets the most specific information attached to a node.
    832     *
    833     * @param node the node
    834     * @param info the information
    835     * @return the information or null
    836     * @deprecated 3.2
    837     */
    838    @Deprecated
    839    public static JexlInfo getInfo(final JexlNode node, final JexlInfo info) {
    840        return detailedInfo(node, info);
    841    }
    842
    843    /**
    844     * Merge the node info and the cause info to get the best possible location.
    845     *
    846     * @param info  the node
    847     * @param cause the cause
    848     * @return the info to use
    849     */
    850    static JexlInfo merge(final JexlInfo info, final JavaccError cause) {
    851        if (cause == null || cause.getLine() < 0) {
    852            return info;
    853        }
    854        if (info == null) {
    855            return new JexlInfo("", cause.getLine(), cause.getColumn());
    856        }
    857        return new JexlInfo(info.getName(), cause.getLine(), cause.getColumn());
    858    }
    859
    860    /**
    861     * Generates a message for an unsolvable method error.
    862     *
    863     * @param node the node where the error occurred
    864     * @param method the method name
    865     * @return the error message
    866     * @deprecated 3.2
    867     */
    868    @Deprecated
    869    public static String methodError(final JexlNode node, final String method) {
    870        return methodError(node, method, null);
    871    }
    872
    873    /**
    874     * Generates a message for an unsolvable method error.
    875     *
    876     * @param node the node where the error occurred
    877     * @param method the method name
    878     * @param args the method arguments
    879     * @return the error message
    880     */
    881    public static String methodError(final JexlNode node, final String method, final Object[] args) {
    882        final StringBuilder msg = errorAt(node);
    883        msg.append("unsolvable function/method '");
    884        msg.append(methodSignature(method, args));
    885        msg.append('\'');
    886        return msg.toString();
    887    }
    888
    889    /**
    890     * Creates a signed-name for a given method name and arguments.
    891     *
    892     * @param name the method name
    893     * @param args the method arguments
    894     * @return a suitable signed name
    895     */
    896     static String methodSignature(final String name, final Object[] args) {
    897        if (args != null && args.length > 0) {
    898            final StringBuilder strb = new StringBuilder(name);
    899            strb.append('(');
    900            for (int a = 0; a < args.length; ++a) {
    901                if (a > 0) {
    902                    strb.append(", ");
    903                }
    904                final Class<?> clazz = args[a] == null ? Object.class : args[a].getClass();
    905                strb.append(clazz.getSimpleName());
    906            }
    907            strb.append(')');
    908            return strb.toString();
    909        }
    910        return name;
    911    }
    912
    913    /**
    914     * Generates a message for an operator error.
    915     *
    916     * @param node the node where the error occurred
    917     * @param symbol the operator name
    918     * @return the error message
    919     */
    920    public static String operatorError(final JexlNode node, final String symbol) {
    921        final StringBuilder msg = errorAt(node);
    922        msg.append("error calling operator '");
    923        msg.append(symbol);
    924        msg.append('\'');
    925        return msg.toString();
    926    }
    927
    928    /**
    929     * Generates a message for an unsolvable property error.
    930     *
    931     * @param node the node where the error occurred
    932     * @param var the variable
    933     * @return the error message
    934     * @deprecated 3.2
    935     */
    936    @Deprecated
    937    public static String propertyError(final JexlNode node, final String var) {
    938        return propertyError(node, var, true);
    939    }
    940
    941    /**
    942     * Generates a message for an unsolvable property error.
    943     *
    944     * @param node the node where the error occurred
    945     * @param pty the property
    946     * @param undef whether the property is null or undefined
    947     * @return the error message
    948     */
    949    public static String propertyError(final JexlNode node, final String pty, final boolean undef) {
    950        final StringBuilder msg = errorAt(node);
    951        if (undef) {
    952            msg.append("unsolvable");
    953        } else {
    954            msg.append("null value");
    955        }
    956        msg.append(" property '");
    957        msg.append(pty);
    958        msg.append('\'');
    959        return msg.toString();
    960    }
    961
    962    /**
    963     * Removes a slice from a source.
    964     *
    965     * @param src the source
    966     * @param froml the beginning line
    967     * @param fromc the beginning column
    968     * @param tol the ending line
    969     * @param toc the ending column
    970     * @return the source with the zone from (from) to (to) removed
    971     */
    972    public static String sliceSource(final String src, final int froml, final int fromc, final int tol, final int toc) {
    973        final BufferedReader reader = new BufferedReader(new StringReader(src));
    974        final StringBuilder buffer = new StringBuilder();
    975        String line;
    976        int cl = 1;
    977        try {
    978            while ((line = reader.readLine()) != null) {
    979                if (cl < froml || cl > tol) {
    980                    buffer.append(line).append('\n');
    981                } else {
    982                    if (cl == froml) {
    983                        buffer.append(line, 0, fromc - 1);
    984                    }
    985                    if (cl == tol) {
    986                        buffer.append(line.substring(toc + 1));
    987                    }
    988                } // else ignore line
    989                cl += 1;
    990            }
    991        } catch (final IOException xignore) {
    992            //damn the checked exceptions :-)
    993        }
    994        return buffer.toString();
    995    }
    996
    997    /**
    998     * Wrap an invocation exception.
    999     * <p>Return the cause if it is already a JexlException.
    1000     *
    1001     * @param xinvoke the invocation exception
    1002     * @return a JexlException
    1003     */
    1004    public static JexlException tryFailed(final InvocationTargetException xinvoke) {
    1005        final Throwable cause = xinvoke.getCause();
    1006        return cause instanceof JexlException
    1007                ? (JexlException) cause
    1008                : new JexlException.TryFailed(xinvoke); // fail
    1009    }
    1010
    1011    /**
    1012     * Unwraps the cause of a throwable due to reflection.
    1013     *
    1014     * @param xthrow the throwable
    1015     * @return the cause
    1016     */
    1017    static Throwable unwrap(final Throwable xthrow) {
    1018        if (xthrow instanceof TryFailed
    1019            || xthrow instanceof InvocationTargetException
    1020            || xthrow instanceof UndeclaredThrowableException) {
    1021            return xthrow.getCause();
    1022        }
    1023        return xthrow;
    1024    }
    1025
    1026    /**
    1027     * Generates a message for a variable error.
    1028     *
    1029     * @param node the node where the error occurred
    1030     * @param variable the variable
    1031     * @param undef whether the variable is null or undefined
    1032     * @return the error message
    1033     * @deprecated 3.2
    1034     */
    1035    @Deprecated
    1036    public static String variableError(final JexlNode node, final String variable, final boolean undef) {
    1037        return variableError(node, variable, undef? VariableIssue.UNDEFINED : VariableIssue.NULLVALUE);
    1038    }
    1039
    1040    /**
    1041     * Generates a message for a variable error.
    1042     *
    1043     * @param node the node where the error occurred
    1044     * @param variable the variable
    1045     * @param issue  the variable kind of issue
    1046     * @return the error message
    1047     */
    1048    public static String variableError(final JexlNode node, final String variable, final VariableIssue issue) {
    1049        final StringBuilder msg = errorAt(node);
    1050        msg.append(issue.message(variable));
    1051        return msg.toString();
    1052    }
    1053
    1054    /** The point of origin for this exception. */
    1055    private final transient JexlNode mark;
    1056
    1057    /** The debug info. */
    1058    private final transient JexlInfo info;
    1059
    1060    /**
    1061     * Creates a new JexlException.
    1062     *
    1063     * @param jinfo the debugging information associated
    1064     * @param msg   the error message
    1065     * @param cause the exception causing the error
    1066     */
    1067    public JexlException(final JexlInfo jinfo, final String msg, final Throwable cause) {
    1068        super(msg != null ? msg : "", unwrap(cause));
    1069        mark = null;
    1070        info = jinfo;
    1071    }
    1072
    1073    /**
    1074     * Creates a new JexlException.
    1075     *
    1076     * @param node the node causing the error
    1077     * @param msg  the error message
    1078     */
    1079    public JexlException(final JexlNode node, final String msg) {
    1080        this(node, msg, null);
    1081    }
    1082
    1083    /**
    1084     * Creates a new JexlException.
    1085     *
    1086     * @param node  the node causing the error
    1087     * @param msg   the error message
    1088     * @param cause the exception causing the error
    1089     */
    1090    public JexlException(final JexlNode node, final String msg, final Throwable cause) {
    1091        this(node, msg != null ? msg : "", unwrap(cause), true);
    1092    }
    1093
    1094    /**
    1095     * Creates a new JexlException.
    1096     *
    1097     * @param node  the node causing the error
    1098     * @param msg   the error message
    1099     * @param cause the exception causing the error
    1100     * @param trace whether this exception has a stack trace and can <em>not</em> be suppressed
    1101     */
    1102    protected JexlException(final JexlNode node, final String msg, final Throwable cause, final boolean trace) {
    1103        super(msg != null ? msg : "", unwrap(cause), !trace, trace);
    1104        if (node != null) {
    1105            mark = node;
    1106            info = node.jexlInfo();
    1107        } else {
    1108            mark = null;
    1109            info = null;
    1110        }
    1111    }
    1112
    1113    /**
    1114     * Cleans a JexlException from cluttering stack trace elements.
    1115     *
    1116     * @return this exception
    1117     */
    1118    public JexlException clean() {
    1119        return clean(this);
    1120    }
    1121
    1122    /**
    1123     * Accesses the detailed message.
    1124     *
    1125     * @return the message
    1126     */
    1127    protected String detailedMessage() {
    1128        final Class<? extends JexlException> clazz = getClass();
    1129        final String name = clazz == JexlException.class? "JEXL" : clazz.getSimpleName().toLowerCase();
    1130        return name + " error : " + getDetail();
    1131    }
    1132
    1133    /**
    1134     * Gets the exception-specific detail.
    1135     *
    1136     * @return this exception-specific detail
    1137     * @since 3.2
    1138     */
    1139    public final String getDetail() {
    1140        return super.getMessage();
    1141    }
    1142
    1143    /**
    1144     * Gets the specific information for this exception.
    1145     *
    1146     * @return the information
    1147     */
    1148    public JexlInfo getInfo() {
    1149        return detailedInfo(mark, info);
    1150    }
    1151
    1152    /**
    1153     * Detailed info message about this error.
    1154     * <p>
    1155     * Format is "debug![begin,end]: string \n msg" where:
    1156     * </p>
    1157     * <ul>
    1158     * <li>debug is the debugging information if it exists {@link JexlBuilder#debug(boolean)}</li>
    1159     * <li>begin, end are character offsets in the string for the precise location of the error</li>
    1160     * <li>string is the string representation of the offending expression</li>
    1161     * <li>msg is the actual explanation message for this error</li>
    1162     * </ul>
    1163     *
    1164     * @see JexlEngine#isDebug()
    1165     * @return this error as a string
    1166     */
    1167    @Override
    1168    public String getMessage() {
    1169        final StringBuilder msg = new StringBuilder();
    1170        if (info != null) {
    1171            msg.append(info);
    1172        } else {
    1173            msg.append("?:");
    1174        }
    1175        msg.append(' ');
    1176        msg.append(detailedMessage());
    1177        final Throwable cause = getCause();
    1178        if (cause instanceof JexlArithmetic.NullOperand) {
    1179            msg.append(" caused by null operand");
    1180        }
    1181        return msg.toString();
    1182    }
    1183
    1184    /**
    1185     * Pleasing checkstyle.
    1186     *
    1187     * @return the info
    1188     */
    1189    protected JexlInfo info() {
    1190        return info;
    1191    }
    1192
    1193    /**
    1194     * Formats an error message from the parser.
    1195     *
    1196     * @param prefix the prefix to the message
    1197     * @param expr   the expression in error
    1198     * @return the formatted message
    1199     */
    1200    protected String parserError(final String prefix, final String expr) {
    1201        final int length = expr.length();
    1202        if (length < MAX_EXCHARLOC) {
    1203            return prefix + " error in '" + expr + "'";
    1204        }
    1205        final int me = MAX_EXCHARLOC / 2;
    1206        int begin = info.getColumn() - me;
    1207        if (begin < 0 || length < me) {
    1208            begin = 0;
    1209        } else if (begin > length) {
    1210            begin = me;
    1211        }
    1212        int end = begin + MAX_EXCHARLOC;
    1213        if (end > length) {
    1214            end = length;
    1215        }
    1216        return prefix + " error near '... "
    1217                + expr.substring(begin, end) + " ...'";
    1218    }
    1219}