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         * 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     * Format is "debug![begin,end]: string \n msg" where:
1073     * - debug is the debugging information if it exists (@link JexlEngine.setDebug)
1074     * - begin, end are character offsets in the string for the precise location of the error
1075     * - string is the string representation of the offending expression
1076     * - msg is the actual explanation message for this error
1077     *
1078     * @return this error as a string
1079     */
1080    @Override
1081    public String getMessage() {
1082        final StringBuilder msg = new StringBuilder();
1083        if (info != null) {
1084            msg.append(info.toString());
1085        } else {
1086            msg.append("?:");
1087        }
1088        msg.append(' ');
1089        msg.append(detailedMessage());
1090        final Throwable cause = getCause();
1091        if (cause instanceof JexlArithmetic.NullOperand) {
1092            msg.append(" caused by null operand");
1093        }
1094        return msg.toString();
1095    }
1096
1097    /**
1098     * Pleasing checkstyle.
1099     * @return the info
1100     */
1101    protected JexlInfo info() {
1102        return info;
1103    }
1104
1105    /**
1106     * Formats an error message from the parser.
1107     *
1108     * @param prefix the prefix to the message
1109     * @param expr   the expression in error
1110     * @return the formatted message
1111     */
1112    protected String parserError(final String prefix, final String expr) {
1113        final int length = expr.length();
1114        if (length < MAX_EXCHARLOC) {
1115            return prefix + " error in '" + expr + "'";
1116        }
1117        final int me = MAX_EXCHARLOC / 2;
1118        int begin = info.getColumn() - me;
1119        if (begin < 0 || length < me) {
1120            begin = 0;
1121        } else if (begin > length) {
1122            begin = me;
1123        }
1124        int end = begin + MAX_EXCHARLOC;
1125        if (end > length) {
1126            end = length;
1127        }
1128        return prefix + " error near '... "
1129                + expr.substring(begin, end) + " ...'";
1130    }
1131}