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