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     * Enumerates types of variable issues.
674     */
675    public enum VariableIssue {
676
677        /** The variable is undefined. */
678        UNDEFINED,
679
680        /** The variable is already declared. */
681        REDEFINED,
682
683        /** The variable has a null value. */
684        NULLVALUE,
685
686        /** THe variable is const and an attempt is made to assign it*/
687        CONST;
688
689        /**
690         * Stringifies the variable issue.
691         *
692         * @param var the variable name
693         * @return the issue message
694         */
695        public String message(final String var) {
696            switch(this) {
697                case NULLVALUE : return VARQUOTE + var + "' is null";
698                case REDEFINED : return VARQUOTE + var + "' is already defined";
699                case CONST : return VARQUOTE + var + "' is const";
700                case UNDEFINED :
701                default: return VARQUOTE + var + "' is undefined";
702            }
703        }
704    }
705
706    private static final long serialVersionUID = 20210606123900L;
707
708    /** Maximum number of characters around exception location. */
709    private static final int MAX_EXCHARLOC = 128;
710
711    /** Used 3 times. */
712    private static final String VARQUOTE = "variable '";
713
714    /**
715     * Generates a message for an annotation error.
716     *
717     * @param node the node where the error occurred
718     * @param annotation the annotation name
719     * @return the error message
720     * @since 3.1
721     */
722    public static String annotationError(final JexlNode node, final String annotation) {
723        final StringBuilder msg = errorAt(node);
724        msg.append("error processing annotation '");
725        msg.append(annotation);
726        msg.append('\'');
727        return msg.toString();
728    }
729
730    /**
731     * Cleans a Throwable from cluttering stack trace elements.
732     *
733     * @param <X>    the throwable type
734     * @param xthrow the throwable
735     * @return the throwable
736     */
737     static <X extends Throwable> X clean(final X xthrow) {
738        if (xthrow != null) {
739            final List<StackTraceElement> stackJexl = new ArrayList<>();
740            for (final StackTraceElement se : xthrow.getStackTrace()) {
741                final String className = se.getClassName();
742                if (startsWithAny(CLEAN_STOP, className)) {
743                    break;
744                }
745                if (!startsWithAny(CLEAN_SKIP, className)) {
746                    stackJexl.add(se);
747                }
748            }
749            xthrow.setStackTrace(stackJexl.toArray(EMPTY_STACK_TRACE_ELEMENT_ARRAY));
750        }
751        return xthrow;
752    }
753
754    /**
755     * Checks whether a given name starts with any of the given prefixes.
756     * @param prefixes the prefixes to check
757     * @param name the name to check
758     * @return true if the name starts with any of the prefixes, false otherwise
759     */
760    private static boolean startsWithAny(final String[] prefixes, final String name) {
761        for (final String prefix : prefixes) {
762            if (name.startsWith(prefix)) {
763                return true;
764            }
765        }
766        return false;
767    }
768
769    /**
770     * A static array of package names that will be skipped during
771     * exception stack-trace cleansing.
772     */
773    private static final String[] CLEAN_SKIP = {
774      "org.apache.commons.jexl3.internal",
775      "org.apache.commons.jexl3.parser",
776      "java.lang.reflect",
777      "sun.reflect"
778    };
779
780    /**
781     * A static array of package names that will be considered as a stop point during
782     * exception stack-trace cleansing.
783     * <p>
784     * All stacktrace elements occurring after one of these packages matches an element class name
785     * will be discarded.
786     * </p>
787     */
788    private static final String[] CLEAN_STOP = { "org.junit" };
789
790    /**
791     * Gets the most specific information attached to a node.
792     *
793     * @param node the node
794     * @param info the information
795     * @return the information or null
796     */
797     static JexlInfo detailedInfo(final JexlNode node, final JexlInfo info) {
798        if (info != null && node != null) {
799            final Debugger dbg = new Debugger();
800            if (dbg.debug(node)) {
801                return new JexlInfo(info) {
802                    @Override
803                    public JexlInfo.Detail getDetail() {
804                        return dbg;
805                    }
806                };
807            }
808        }
809        return info;
810    }
811
812    /**
813     * Creates a string builder pre-filled with common error information (if possible).
814     *
815     * @param node the node
816     * @return a string builder
817     */
818     static StringBuilder errorAt(final JexlNode node) {
819        final JexlInfo info = node != null ? detailedInfo(node, node.jexlInfo()) : null;
820        final StringBuilder msg = new StringBuilder();
821        if (info != null) {
822            msg.append(info);
823        } else {
824            msg.append("?:");
825        }
826        msg.append(' ');
827        return msg;
828    }
829
830    /**
831     * Gets the most specific information attached to a node.
832     *
833     * @param node the node
834     * @param info the information
835     * @return the information or null
836     * @deprecated 3.2
837     */
838    @Deprecated
839    public static JexlInfo getInfo(final JexlNode node, final JexlInfo info) {
840        return detailedInfo(node, info);
841    }
842
843    /**
844     * Merge the node info and the cause info to get the best possible location.
845     *
846     * @param info  the node
847     * @param cause the cause
848     * @return the info to use
849     */
850    static JexlInfo merge(final JexlInfo info, final JavaccError cause) {
851        if (cause == null || cause.getLine() < 0) {
852            return info;
853        }
854        if (info == null) {
855            return new JexlInfo("", cause.getLine(), cause.getColumn());
856        }
857        return new JexlInfo(info.getName(), cause.getLine(), cause.getColumn());
858    }
859
860    /**
861     * Generates a message for an unsolvable method error.
862     *
863     * @param node the node where the error occurred
864     * @param method the method name
865     * @return the error message
866     * @deprecated 3.2
867     */
868    @Deprecated
869    public static String methodError(final JexlNode node, final String method) {
870        return methodError(node, method, null);
871    }
872
873    /**
874     * Generates a message for an unsolvable method error.
875     *
876     * @param node the node where the error occurred
877     * @param method the method name
878     * @param args the method arguments
879     * @return the error message
880     */
881    public static String methodError(final JexlNode node, final String method, final Object[] args) {
882        final StringBuilder msg = errorAt(node);
883        msg.append("unsolvable function/method '");
884        msg.append(methodSignature(method, args));
885        msg.append('\'');
886        return msg.toString();
887    }
888
889    /**
890     * Creates a signed-name for a given method name and arguments.
891     *
892     * @param name the method name
893     * @param args the method arguments
894     * @return a suitable signed name
895     */
896     static String methodSignature(final String name, final Object[] args) {
897        if (args != null && args.length > 0) {
898            final StringBuilder strb = new StringBuilder(name);
899            strb.append('(');
900            for (int a = 0; a < args.length; ++a) {
901                if (a > 0) {
902                    strb.append(", ");
903                }
904                final Class<?> clazz = args[a] == null ? Object.class : args[a].getClass();
905                strb.append(clazz.getSimpleName());
906            }
907            strb.append(')');
908            return strb.toString();
909        }
910        return name;
911    }
912
913    /**
914     * Generates a message for an operator error.
915     *
916     * @param node the node where the error occurred
917     * @param symbol the operator name
918     * @return the error message
919     */
920    public static String operatorError(final JexlNode node, final String symbol) {
921        final StringBuilder msg = errorAt(node);
922        msg.append("error calling operator '");
923        msg.append(symbol);
924        msg.append('\'');
925        return msg.toString();
926    }
927
928    /**
929     * Generates a message for an unsolvable property error.
930     *
931     * @param node the node where the error occurred
932     * @param var the variable
933     * @return the error message
934     * @deprecated 3.2
935     */
936    @Deprecated
937    public static String propertyError(final JexlNode node, final String var) {
938        return propertyError(node, var, true);
939    }
940
941    /**
942     * Generates a message for an unsolvable property error.
943     *
944     * @param node the node where the error occurred
945     * @param pty the property
946     * @param undef whether the property is null or undefined
947     * @return the error message
948     */
949    public static String propertyError(final JexlNode node, final String pty, final boolean undef) {
950        final StringBuilder msg = errorAt(node);
951        if (undef) {
952            msg.append("unsolvable");
953        } else {
954            msg.append("null value");
955        }
956        msg.append(" property '");
957        msg.append(pty);
958        msg.append('\'');
959        return msg.toString();
960    }
961
962    /**
963     * Removes a slice from a source.
964     *
965     * @param src the source
966     * @param froml the beginning line
967     * @param fromc the beginning column
968     * @param tol the ending line
969     * @param toc the ending column
970     * @return the source with the zone from (from) to (to) removed
971     */
972    public static String sliceSource(final String src, final int froml, final int fromc, final int tol, final int toc) {
973        final BufferedReader reader = new BufferedReader(new StringReader(src));
974        final StringBuilder buffer = new StringBuilder();
975        String line;
976        int cl = 1;
977        try {
978            while ((line = reader.readLine()) != null) {
979                if (cl < froml || cl > tol) {
980                    buffer.append(line).append('\n');
981                } else {
982                    if (cl == froml) {
983                        buffer.append(line, 0, fromc - 1);
984                    }
985                    if (cl == tol) {
986                        buffer.append(line.substring(toc + 1));
987                    }
988                } // else ignore line
989                cl += 1;
990            }
991        } catch (final IOException xignore) {
992            //damn the checked exceptions :-)
993        }
994        return buffer.toString();
995    }
996
997    /**
998     * Wrap an invocation exception.
999     * <p>Return the cause if it is already a JexlException.
1000     *
1001     * @param xinvoke the invocation exception
1002     * @return a JexlException
1003     */
1004    public static JexlException tryFailed(final InvocationTargetException xinvoke) {
1005        final Throwable cause = xinvoke.getCause();
1006        return cause instanceof JexlException
1007                ? (JexlException) cause
1008                : new JexlException.TryFailed(xinvoke); // fail
1009    }
1010
1011    /**
1012     * Unwraps the cause of a throwable due to reflection.
1013     *
1014     * @param xthrow the throwable
1015     * @return the cause
1016     */
1017    static Throwable unwrap(final Throwable xthrow) {
1018        if (xthrow instanceof TryFailed
1019            || xthrow instanceof InvocationTargetException
1020            || xthrow instanceof UndeclaredThrowableException) {
1021            return xthrow.getCause();
1022        }
1023        return xthrow;
1024    }
1025
1026    /**
1027     * Generates a message for a variable error.
1028     *
1029     * @param node the node where the error occurred
1030     * @param variable the variable
1031     * @param undef whether the variable is null or undefined
1032     * @return the error message
1033     * @deprecated 3.2
1034     */
1035    @Deprecated
1036    public static String variableError(final JexlNode node, final String variable, final boolean undef) {
1037        return variableError(node, variable, undef? VariableIssue.UNDEFINED : VariableIssue.NULLVALUE);
1038    }
1039
1040    /**
1041     * Generates a message for a variable error.
1042     *
1043     * @param node the node where the error occurred
1044     * @param variable the variable
1045     * @param issue  the variable kind of issue
1046     * @return the error message
1047     */
1048    public static String variableError(final JexlNode node, final String variable, final VariableIssue issue) {
1049        final StringBuilder msg = errorAt(node);
1050        msg.append(issue.message(variable));
1051        return msg.toString();
1052    }
1053
1054    /** The point of origin for this exception. */
1055    private final transient JexlNode mark;
1056
1057    /** The debug info. */
1058    private final transient JexlInfo info;
1059
1060    /**
1061     * Creates a new JexlException.
1062     *
1063     * @param jinfo the debugging information associated
1064     * @param msg   the error message
1065     * @param cause the exception causing the error
1066     */
1067    public JexlException(final JexlInfo jinfo, final String msg, final Throwable cause) {
1068        super(msg != null ? msg : "", unwrap(cause));
1069        mark = null;
1070        info = jinfo;
1071    }
1072
1073    /**
1074     * Creates a new JexlException.
1075     *
1076     * @param node the node causing the error
1077     * @param msg  the error message
1078     */
1079    public JexlException(final JexlNode node, final String msg) {
1080        this(node, msg, null);
1081    }
1082
1083    /**
1084     * Creates a new JexlException.
1085     *
1086     * @param node  the node causing the error
1087     * @param msg   the error message
1088     * @param cause the exception causing the error
1089     */
1090    public JexlException(final JexlNode node, final String msg, final Throwable cause) {
1091        this(node, msg != null ? msg : "", unwrap(cause), true);
1092    }
1093
1094    /**
1095     * Creates a new JexlException.
1096     *
1097     * @param node  the node causing the error
1098     * @param msg   the error message
1099     * @param cause the exception causing the error
1100     * @param trace whether this exception has a stack trace and can <em>not</em> be suppressed
1101     */
1102    protected JexlException(final JexlNode node, final String msg, final Throwable cause, final boolean trace) {
1103        super(msg != null ? msg : "", unwrap(cause), !trace, trace);
1104        if (node != null) {
1105            mark = node;
1106            info = node.jexlInfo();
1107        } else {
1108            mark = null;
1109            info = null;
1110        }
1111    }
1112
1113    /**
1114     * Cleans a JexlException from cluttering stack trace elements.
1115     *
1116     * @return this exception
1117     */
1118    public JexlException clean() {
1119        return clean(this);
1120    }
1121
1122    /**
1123     * Accesses the detailed message.
1124     *
1125     * @return the message
1126     */
1127    protected String detailedMessage() {
1128        final Class<? extends JexlException> clazz = getClass();
1129        final String name = clazz == JexlException.class? "JEXL" : clazz.getSimpleName().toLowerCase();
1130        return name + " error : " + getDetail();
1131    }
1132
1133    /**
1134     * Gets the exception-specific detail.
1135     *
1136     * @return this exception-specific detail
1137     * @since 3.2
1138     */
1139    public final String getDetail() {
1140        return super.getMessage();
1141    }
1142
1143    /**
1144     * Gets the specific information for this exception.
1145     *
1146     * @return the information
1147     */
1148    public JexlInfo getInfo() {
1149        return detailedInfo(mark, info);
1150    }
1151
1152    /**
1153     * Detailed info message about this error.
1154     * <p>
1155     * Format is "debug![begin,end]: string \n msg" where:
1156     * </p>
1157     * <ul>
1158     * <li>debug is the debugging information if it exists {@link JexlBuilder#debug(boolean)}</li>
1159     * <li>begin, end are character offsets in the string for the precise location of the error</li>
1160     * <li>string is the string representation of the offending expression</li>
1161     * <li>msg is the actual explanation message for this error</li>
1162     * </ul>
1163     *
1164     * @see JexlEngine#isDebug()
1165     * @return this error as a string
1166     */
1167    @Override
1168    public String getMessage() {
1169        final StringBuilder msg = new StringBuilder();
1170        if (info != null) {
1171            msg.append(info);
1172        } else {
1173            msg.append("?:");
1174        }
1175        msg.append(' ');
1176        msg.append(detailedMessage());
1177        final Throwable cause = getCause();
1178        if (cause instanceof JexlArithmetic.NullOperand) {
1179            msg.append(" caused by null operand");
1180        }
1181        return msg.toString();
1182    }
1183
1184    /**
1185     * Pleasing checkstyle.
1186     *
1187     * @return the info
1188     */
1189    protected JexlInfo info() {
1190        return info;
1191    }
1192
1193    /**
1194     * Formats an error message from the parser.
1195     *
1196     * @param prefix the prefix to the message
1197     * @param expr   the expression in error
1198     * @return the formatted message
1199     */
1200    protected String parserError(final String prefix, final String expr) {
1201        final int length = expr.length();
1202        if (length < MAX_EXCHARLOC) {
1203            return prefix + " error in '" + expr + "'";
1204        }
1205        final int me = MAX_EXCHARLOC / 2;
1206        int begin = info.getColumn() - me;
1207        if (begin < 0 || length < me) {
1208            begin = 0;
1209        } else if (begin > length) {
1210            begin = me;
1211        }
1212        int end = begin + MAX_EXCHARLOC;
1213        if (end > length) {
1214            end = length;
1215        }
1216        return prefix + " error near '... "
1217                + expr.substring(begin, end) + " ...'";
1218    }
1219}