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