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}