View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.jexl3;
19  
20  import java.io.BufferedReader;
21  import java.io.IOException;
22  import java.io.StringReader;
23  import java.lang.reflect.InvocationTargetException;
24  import java.lang.reflect.UndeclaredThrowableException;
25  import java.util.ArrayList;
26  import java.util.List;
27  import java.util.Objects;
28  
29  import org.apache.commons.jexl3.internal.Debugger;
30  import org.apache.commons.jexl3.parser.JavaccError;
31  import org.apache.commons.jexl3.parser.JexlNode;
32  import org.apache.commons.jexl3.parser.ParseException;
33  import org.apache.commons.jexl3.parser.TokenMgrException;
34  
35  /**
36   * Wraps any error that might occur during interpretation of a script or expression.
37   *
38   * @since 2.0
39   */
40  public class JexlException extends RuntimeException {
41      /**
42       * Thrown when parsing fails due to an ambiguous statement.
43       *
44       * @since 3.0
45       */
46      public static class Ambiguous extends Parsing {
47          private static final long serialVersionUID = 20210606123903L;
48          /** The mark at which ambiguity might stop and recover. */
49          private final transient JexlInfo recover;
50          /**
51           * Creates a new Ambiguous statement exception instance.
52           * @param begin  the start location information
53           * @param end the end location information
54           * @param expr  the source expression line
55           */
56          public Ambiguous(final JexlInfo begin, final JexlInfo end, final String expr) {
57              super(begin, expr);
58              recover = end;
59          }
60  
61          /**
62           * Creates a new Ambiguous statement exception instance.
63           * @param info  the location information
64           * @param expr  the source expression line
65           */
66          public Ambiguous(final JexlInfo info, final String expr) {
67             this(info, null, expr);
68          }
69  
70          @Override
71          protected String detailedMessage() {
72              return parserError("ambiguous statement", getDetail());
73          }
74  
75          /**
76           * Tries to remove this ambiguity in the source.
77           * @param src the source that triggered this exception
78           * @return the source with the ambiguous statement removed
79           *         or null if no recovery was possible
80           */
81          public String tryCleanSource(final String src) {
82              final JexlInfo ji = info();
83              return ji == null || recover == null
84                    ? src
85                    : sliceSource(src, ji.getLine(), ji.getColumn(), recover.getLine(), recover.getColumn());
86          }
87      }
88  
89      /**
90       * Thrown when an annotation handler throws an exception.
91       *
92       * @since 3.1
93       */
94      public static class Annotation extends JexlException {
95          private static final long serialVersionUID = 20210606124101L;
96          /**
97           * Creates a new Annotation exception instance.
98           *
99           * @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 }