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