001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      https://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.jexl3;
019
020import java.io.BufferedReader;
021import java.io.IOException;
022import java.io.StringReader;
023import java.lang.reflect.InvocationTargetException;
024import java.lang.reflect.UndeclaredThrowableException;
025import java.util.ArrayList;
026import java.util.List;
027import java.util.Objects;
028
029import org.apache.commons.jexl3.internal.Debugger;
030import org.apache.commons.jexl3.parser.JavaccError;
031import org.apache.commons.jexl3.parser.JexlNode;
032import org.apache.commons.jexl3.parser.ParseException;
033import org.apache.commons.jexl3.parser.TokenMgrException;
034
035/**
036 * Wraps any error that might occur during interpretation of a script or expression.
037 *
038 * @since 2.0
039 */
040public class JexlException extends RuntimeException {
041
042    private static final StackTraceElement[] EMPTY_STACK_TRACE_ELEMENT_ARRAY = {};
043
044    /**
045     * Thrown when parsing fails due to an ambiguous statement.
046     *
047     * @since 3.0
048     */
049    public static class Ambiguous extends Parsing {
050        private static final long serialVersionUID = 20210606123903L;
051
052        /** The mark at which ambiguity might stop and recover. */
053        private final transient JexlInfo recover;
054
055        /**
056         * Creates a new Ambiguous statement exception instance.
057         *
058         * @param begin  the start location information
059         * @param end the end location information
060         * @param expr  the source expression line
061         */
062        public Ambiguous(final JexlInfo begin, final JexlInfo end, final String expr) {
063            super(begin, expr);
064            recover = end;
065        }
066
067        /**
068         * Creates a new Ambiguous statement exception instance.
069         *
070         * @param info  the location information
071         * @param expr  the source expression line
072         */
073        public Ambiguous(final JexlInfo info, final String expr) {
074           this(info, null, expr);
075        }
076
077        @Override
078        protected String detailedMessage() {
079            return parserError("ambiguous statement", getDetail());
080        }
081
082        /**
083         * Tries to remove this ambiguity in the source.
084         *
085         * @param src the source that triggered this exception
086         * @return the source with the ambiguous statement removed
087         *         or null if no recovery was possible
088         */
089        public String tryCleanSource(final String src) {
090            final JexlInfo ji = info();
091            return ji == null || recover == null
092                  ? src
093                  : sliceSource(src, ji.getLine(), ji.getColumn(), recover.getLine(), recover.getColumn());
094        }
095    }
096
097    /**
098     * Thrown when an annotation handler throws an exception.
099     *
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}