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      * The various type 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 any org.apache.commons.jexl3.internal stack trace element.
732      *
733      * @param <X>    the throwable type
734      * @param xthrow the thowable
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 (!className.startsWith("org.apache.commons.jexl3.internal")
743                         && !className.startsWith("org.apache.commons.jexl3.parser")) {
744                     stackJexl.add(se);
745                 }
746             }
747             xthrow.setStackTrace(stackJexl.toArray(EMPTY_STACK_TRACE_ELEMENT_ARRAY));
748         }
749         return xthrow;
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      */
759      static JexlInfo detailedInfo(final JexlNode node, final JexlInfo info) {
760         if (info != null && node != null) {
761             final Debugger dbg = new Debugger();
762             if (dbg.debug(node)) {
763                 return new JexlInfo(info) {
764                     @Override
765                     public JexlInfo.Detail getDetail() {
766                         return dbg;
767                     }
768                 };
769             }
770         }
771         return info;
772     }
773 
774     /**
775      * Creates a string builder pre-filled with common error information (if possible).
776      *
777      * @param node the node
778      * @return a string builder
779      */
780      static StringBuilder errorAt(final JexlNode node) {
781         final JexlInfo info = node != null ? detailedInfo(node, node.jexlInfo()) : null;
782         final StringBuilder msg = new StringBuilder();
783         if (info != null) {
784             msg.append(info.toString());
785         } else {
786             msg.append("?:");
787         }
788         msg.append(' ');
789         return msg;
790     }
791 
792     /**
793      * Gets the most specific information attached to a node.
794      *
795      * @param node the node
796      * @param info the information
797      * @return the information or null
798      * @deprecated 3.2
799      */
800     @Deprecated
801     public static JexlInfo getInfo(final JexlNode node, final JexlInfo info) {
802         return detailedInfo(node, info);
803     }
804 
805     /**
806      * Merge the node info and the cause info to obtain the best possible location.
807      *
808      * @param info  the node
809      * @param cause the cause
810      * @return the info to use
811      */
812     static JexlInfo merge(final JexlInfo info, final JavaccError cause) {
813         if (cause == null || cause.getLine() < 0) {
814             return info;
815         }
816         if (info == null) {
817             return new JexlInfo("", cause.getLine(), cause.getColumn());
818         }
819         return new JexlInfo(info.getName(), cause.getLine(), cause.getColumn());
820     }
821 
822     /**
823      * Generates a message for a unsolvable method error.
824      *
825      * @param node the node where the error occurred
826      * @param method the method name
827      * @return the error message
828      * @deprecated 3.2
829      */
830     @Deprecated
831     public static String methodError(final JexlNode node, final String method) {
832         return methodError(node, method, null);
833     }
834 
835     /**
836      * Generates a message for a unsolvable method error.
837      *
838      * @param node the node where the error occurred
839      * @param method the method name
840      * @param args the method arguments
841      * @return the error message
842      */
843     public static String methodError(final JexlNode node, final String method, final Object[] args) {
844         final StringBuilder msg = errorAt(node);
845         msg.append("unsolvable function/method '");
846         msg.append(methodSignature(method, args));
847         msg.append('\'');
848         return msg.toString();
849     }
850 
851     /**
852      * Creates a signed-name for a given method name and arguments.
853      *
854      * @param name the method name
855      * @param args the method arguments
856      * @return a suitable signed name
857      */
858      static String methodSignature(final String name, final Object[] args) {
859         if (args != null && args.length > 0) {
860             final StringBuilder strb = new StringBuilder(name);
861             strb.append('(');
862             for (int a = 0; a < args.length; ++a) {
863                 if (a > 0) {
864                     strb.append(", ");
865                 }
866                 final Class<?> clazz = args[a] == null ? Object.class : args[a].getClass();
867                 strb.append(clazz.getSimpleName());
868             }
869             strb.append(')');
870             return strb.toString();
871         }
872         return name;
873     }
874 
875     /**
876      * Generates a message for an operator error.
877      *
878      * @param node the node where the error occurred
879      * @param symbol the operator name
880      * @return the error message
881      */
882     public static String operatorError(final JexlNode node, final String symbol) {
883         final StringBuilder msg = errorAt(node);
884         msg.append("error calling operator '");
885         msg.append(symbol);
886         msg.append('\'');
887         return msg.toString();
888     }
889 
890     /**
891      * Generates a message for an unsolvable property error.
892      *
893      * @param node the node where the error occurred
894      * @param var the variable
895      * @return the error message
896      * @deprecated 3.2
897      */
898     @Deprecated
899     public static String propertyError(final JexlNode node, final String var) {
900         return propertyError(node, var, true);
901     }
902 
903     /**
904      * Generates a message for an unsolvable property error.
905      *
906      * @param node the node where the error occurred
907      * @param pty the property
908      * @param undef whether the property is null or undefined
909      * @return the error message
910      */
911     public static String propertyError(final JexlNode node, final String pty, final boolean undef) {
912         final StringBuilder msg = errorAt(node);
913         if (undef) {
914             msg.append("unsolvable");
915         } else {
916             msg.append("null value");
917         }
918         msg.append(" property '");
919         msg.append(pty);
920         msg.append('\'');
921         return msg.toString();
922     }
923 
924     /**
925      * Removes a slice from a source.
926      *
927      * @param src the source
928      * @param froml the beginning line
929      * @param fromc the beginning column
930      * @param tol the ending line
931      * @param toc the ending column
932      * @return the source with the (begin) to (to) zone removed
933      */
934     public static String sliceSource(final String src, final int froml, final int fromc, final int tol, final int toc) {
935         final BufferedReader reader = new BufferedReader(new StringReader(src));
936         final StringBuilder buffer = new StringBuilder();
937         String line;
938         int cl = 1;
939         try {
940             while ((line = reader.readLine()) != null) {
941                 if (cl < froml || cl > tol) {
942                     buffer.append(line).append('\n');
943                 } else {
944                     if (cl == froml) {
945                         buffer.append(line, 0, fromc - 1);
946                     }
947                     if (cl == tol) {
948                         buffer.append(line.substring(toc + 1));
949                     }
950                 } // else ignore line
951                 cl += 1;
952             }
953         } catch (final IOException xignore) {
954             //damn the checked exceptions :-)
955         }
956         return buffer.toString();
957     }
958 
959     /**
960      * Wrap an invocation exception.
961      * <p>Return the cause if it is already a JexlException.
962      *
963      * @param xinvoke the invocation exception
964      * @return a JexlException
965      */
966     public static JexlException tryFailed(final InvocationTargetException xinvoke) {
967         final Throwable cause = xinvoke.getCause();
968         return cause instanceof JexlException
969                 ? (JexlException) cause
970                 : new JexlException.TryFailed(xinvoke); // fail
971     }
972 
973     /**
974      * Unwraps the cause of a throwable due to reflection.
975      *
976      * @param xthrow the throwable
977      * @return the cause
978      */
979     static Throwable unwrap(final Throwable xthrow) {
980         if (xthrow instanceof TryFailed
981             || xthrow instanceof InvocationTargetException
982             || xthrow instanceof UndeclaredThrowableException) {
983             return xthrow.getCause();
984         }
985         return xthrow;
986     }
987 
988     /**
989      * Generates a message for a variable error.
990      *
991      * @param node the node where the error occurred
992      * @param variable the variable
993      * @param undef whether the variable is null or undefined
994      * @return the error message
995      * @deprecated 3.2
996      */
997     @Deprecated
998     public static String variableError(final JexlNode node, final String variable, final boolean undef) {
999         return variableError(node, variable, undef? VariableIssue.UNDEFINED : VariableIssue.NULLVALUE);
1000     }
1001 
1002     /**
1003      * Generates a message for a variable error.
1004      *
1005      * @param node the node where the error occurred
1006      * @param variable the variable
1007      * @param issue  the variable kind of issue
1008      * @return the error message
1009      */
1010     public static String variableError(final JexlNode node, final String variable, final VariableIssue issue) {
1011         final StringBuilder msg = errorAt(node);
1012         msg.append(issue.message(variable));
1013         return msg.toString();
1014     }
1015 
1016     /** The point of origin for this exception. */
1017     private final transient JexlNode mark;
1018 
1019     /** The debug info. */
1020     private final transient JexlInfo info;
1021 
1022     /**
1023      * Creates a new JexlException.
1024      *
1025      * @param jinfo the debugging information associated
1026      * @param msg   the error message
1027      * @param cause the exception causing the error
1028      */
1029     public JexlException(final JexlInfo jinfo, final String msg, final Throwable cause) {
1030         super(msg != null ? msg : "", unwrap(cause));
1031         mark = null;
1032         info = jinfo;
1033     }
1034 
1035     /**
1036      * Creates a new JexlException.
1037      *
1038      * @param node the node causing the error
1039      * @param msg  the error message
1040      */
1041     public JexlException(final JexlNode node, final String msg) {
1042         this(node, msg, null);
1043     }
1044 
1045     /**
1046      * Creates a new JexlException.
1047      *
1048      * @param node  the node causing the error
1049      * @param msg   the error message
1050      * @param cause the exception causing the error
1051      */
1052     public JexlException(final JexlNode node, final String msg, final Throwable cause) {
1053         this(node, msg != null ? msg : "", unwrap(cause), true);
1054     }
1055 
1056     /**
1057      * Creates a new JexlException.
1058      *
1059      * @param node  the node causing the error
1060      * @param msg   the error message
1061      * @param cause the exception causing the error
1062      * @param trace whether this exception has a stack trace and can <em>not</em> be suppressed
1063      */
1064     protected JexlException(final JexlNode node, final String msg, final Throwable cause, final boolean trace) {
1065         super(msg != null ? msg : "", unwrap(cause), !trace, trace);
1066         if (node != null) {
1067             mark = node;
1068             info = node.jexlInfo();
1069         } else {
1070             mark = null;
1071             info = null;
1072         }
1073     }
1074 
1075     /**
1076      * Cleans a JexlException from any org.apache.commons.jexl3.internal stack trace element.
1077      *
1078      * @return this exception
1079      */
1080     public JexlException clean() {
1081         return clean(this);
1082     }
1083 
1084     /**
1085      * Accesses detailed message.
1086      *
1087      * @return the message
1088      */
1089     protected String detailedMessage() {
1090         final Class<? extends JexlException> clazz = getClass();
1091         final String name = clazz == JexlException.class? "JEXL" : clazz.getSimpleName().toLowerCase();
1092         return name + " error : " + getDetail();
1093     }
1094 
1095     /**
1096      * Gets the exception specific detail
1097      *
1098      * @return this exception specific detail
1099      * @since 3.2
1100      */
1101     public final String getDetail() {
1102         return super.getMessage();
1103     }
1104 
1105     /**
1106      * Gets the specific information for this exception.
1107      *
1108      * @return the information
1109      */
1110     public JexlInfo getInfo() {
1111         return detailedInfo(mark, info);
1112     }
1113 
1114     /**
1115      * Detailed info message about this error.
1116      * <p>
1117      * Format is "debug![begin,end]: string \n msg" where:
1118      * </p>
1119      * <ul>
1120      * <li>debug is the debugging information if it exists {@link JexlBuilder#debug(boolean)}</li>
1121      * <li>begin, end are character offsets in the string for the precise location of the error</li>
1122      * <li>string is the string representation of the offending expression</li>
1123      * <li>msg is the actual explanation message for this error</li>
1124      * </ul>
1125      *
1126      * @see JexlEngine#isDebug()
1127      * @return this error as a string
1128      */
1129     @Override
1130     public String getMessage() {
1131         final StringBuilder msg = new StringBuilder();
1132         if (info != null) {
1133             msg.append(info.toString());
1134         } else {
1135             msg.append("?:");
1136         }
1137         msg.append(' ');
1138         msg.append(detailedMessage());
1139         final Throwable cause = getCause();
1140         if (cause instanceof JexlArithmetic.NullOperand) {
1141             msg.append(" caused by null operand");
1142         }
1143         return msg.toString();
1144     }
1145 
1146     /**
1147      * Pleasing checkstyle.
1148      *
1149      * @return the info
1150      */
1151     protected JexlInfo info() {
1152         return info;
1153     }
1154 
1155     /**
1156      * Formats an error message from the parser.
1157      *
1158      * @param prefix the prefix to the message
1159      * @param expr   the expression in error
1160      * @return the formatted message
1161      */
1162     protected String parserError(final String prefix, final String expr) {
1163         final int length = expr.length();
1164         if (length < MAX_EXCHARLOC) {
1165             return prefix + " error in '" + expr + "'";
1166         }
1167         final int me = MAX_EXCHARLOC / 2;
1168         int begin = info.getColumn() - me;
1169         if (begin < 0 || length < me) {
1170             begin = 0;
1171         } else if (begin > length) {
1172             begin = me;
1173         }
1174         int end = begin + MAX_EXCHARLOC;
1175         if (end > length) {
1176             end = length;
1177         }
1178         return prefix + " error near '... "
1179                 + expr.substring(begin, end) + " ...'";
1180     }
1181 }