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