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