1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.commons.jexl3;
19
20 import java.io.BufferedReader;
21 import java.io.IOException;
22 import java.io.StringReader;
23 import java.lang.reflect.InvocationTargetException;
24 import java.lang.reflect.UndeclaredThrowableException;
25 import java.util.ArrayList;
26 import java.util.List;
27 import java.util.Objects;
28
29 import org.apache.commons.jexl3.internal.Debugger;
30 import org.apache.commons.jexl3.parser.JavaccError;
31 import org.apache.commons.jexl3.parser.JexlNode;
32 import org.apache.commons.jexl3.parser.ParseException;
33 import org.apache.commons.jexl3.parser.TokenMgrException;
34
35 /**
36 * Wraps any error that might occur during interpretation of a script or expression.
37 *
38 * @since 2.0
39 */
40 public class JexlException extends RuntimeException {
41
42 private static final StackTraceElement[] EMPTY_STACK_TRACE_ELEMENT_ARRAY = {};
43
44 /**
45 * Thrown when parsing fails due to an ambiguous statement.
46 *
47 * @since 3.0
48 */
49 public static class Ambiguous extends Parsing {
50 private static final long serialVersionUID = 20210606123903L;
51
52 /** The mark at which ambiguity might stop and recover. */
53 private final transient JexlInfo recover;
54
55 /**
56 * Creates a new Ambiguous statement exception instance.
57 *
58 * @param begin the start location information
59 * @param end the end location information
60 * @param expr the source expression line
61 */
62 public Ambiguous(final JexlInfo begin, final JexlInfo end, final String expr) {
63 super(begin, expr);
64 recover = end;
65 }
66
67 /**
68 * Creates a new Ambiguous statement exception instance.
69 *
70 * @param info the location information
71 * @param expr the source expression line
72 */
73 public Ambiguous(final JexlInfo info, final String expr) {
74 this(info, null, expr);
75 }
76
77 @Override
78 protected String detailedMessage() {
79 return parserError("ambiguous statement", getDetail());
80 }
81
82 /**
83 * Tries to remove this ambiguity in the source.
84 *
85 * @param src the source that triggered this exception
86 * @return the source with the ambiguous statement removed
87 * or null if no recovery was possible
88 */
89 public String tryCleanSource(final String src) {
90 final JexlInfo ji = info();
91 return ji == null || recover == null
92 ? src
93 : sliceSource(src, ji.getLine(), ji.getColumn(), recover.getLine(), recover.getColumn());
94 }
95 }
96
97 /**
98 * Thrown when an annotation handler throws an exception.
99 *
100 * @since 3.1
101 */
102 public static class Annotation extends JexlException {
103 private static final long serialVersionUID = 20210606124101L;
104
105 /**
106 * Creates a new Annotation exception instance.
107 *
108 * @param node the annotated statement node
109 * @param name the annotation name
110 * @param cause the exception causing the error
111 */
112 public Annotation(final JexlNode node, final String name, final Throwable cause) {
113 super(node, name, cause);
114 }
115
116 @Override
117 protected String detailedMessage() {
118 return "error processing annotation '" + getAnnotation() + "'";
119 }
120
121 /**
122 * Gets the annotation name
123 *
124 * @return the annotation name
125 */
126 public String getAnnotation() {
127 return getDetail();
128 }
129 }
130
131 /**
132 * Thrown when parsing fails due to an invalid assignment.
133 *
134 * @since 3.0
135 */
136 public static class Assignment extends Parsing {
137 private static final long serialVersionUID = 20210606123905L;
138
139 /**
140 * Creates a new Assignment statement exception instance.
141 *
142 * @param info the location information
143 * @param expr the source expression line
144 */
145 public Assignment(final JexlInfo info, final String expr) {
146 super(info, expr);
147 }
148
149 @Override
150 protected String detailedMessage() {
151 return parserError("assignment", getDetail());
152 }
153 }
154
155 /**
156 * Thrown to break a loop.
157 *
158 * @since 3.0
159 */
160 public static class Break extends JexlException {
161 private static final long serialVersionUID = 20210606124103L;
162
163 /**
164 * Creates a new instance of Break.
165 *
166 * @param node the break
167 */
168 public Break(final JexlNode node) {
169 super(node, "break loop", null, false);
170 }
171 }
172
173 /**
174 * Thrown to cancel a script execution.
175 *
176 * @since 3.0
177 */
178 public static class Cancel extends JexlException {
179 private static final long serialVersionUID = 7735706658499597964L;
180
181 /**
182 * Creates a new instance of Cancel.
183 *
184 * @param node the node where the interruption was detected
185 */
186 public Cancel(final JexlNode node) {
187 super(node, "execution cancelled", null);
188 }
189 }
190
191 /**
192 * Thrown to continue a loop.
193 *
194 * @since 3.0
195 */
196 public static class Continue extends JexlException {
197 private static final long serialVersionUID = 20210606124104L;
198
199 /**
200 * Creates a new instance of Continue.
201 *
202 * @param node the continue-node
203 */
204 public Continue(final JexlNode node) {
205 super(node, "continue loop", null, false);
206 }
207 }
208
209 /**
210 * Thrown when parsing fails due to a disallowed feature.
211 *
212 * @since 3.2
213 */
214 public static class Feature extends Parsing {
215 private static final long serialVersionUID = 20210606123906L;
216
217 /** The feature code. */
218 private final int code;
219
220 /**
221 * Creates a new Ambiguous statement exception instance.
222 *
223 * @param info the location information
224 * @param feature the feature code
225 * @param expr the source expression line
226 */
227 public Feature(final JexlInfo info, final int feature, final String expr) {
228 super(info, expr);
229 this.code = feature;
230 }
231
232 @Override
233 protected String detailedMessage() {
234 return parserError(JexlFeatures.stringify(code), getDetail());
235 }
236 }
237
238 /**
239 * Thrown when a method or ctor is unknown, ambiguous, or inaccessible.
240 *
241 * @since 3.0
242 */
243 public static class Method extends JexlException {
244 private static final long serialVersionUID = 20210606123909L;
245
246 /**
247 * Creates a new Method exception instance.
248 *
249 * @param info the location information
250 * @param name the method name
251 * @param args the method arguments
252 * @since 3.2
253 */
254 public Method(final JexlInfo info, final String name, final Object[] args) {
255 this(info, name, args, null);
256 }
257
258 /**
259 * Creates a new Method exception instance.
260 *
261 * @param info the location information
262 * @param name the method name
263 * @param cause the exception causing the error
264 * @param args the method arguments
265 * @since 3.2
266 */
267 public Method(final JexlInfo info, final String name, final Object[] args, final Throwable cause) {
268 super(info, methodSignature(name, args), cause);
269 }
270
271 /**
272 * Creates a new Method exception instance.
273 *
274 * @param info the location information
275 * @param name the unknown method
276 * @param cause the exception causing the error
277 * @deprecated as of 3.2, use call with method arguments
278 */
279 @Deprecated
280 public Method(final JexlInfo info, final String name, final Throwable cause) {
281 this(info, name, null, cause);
282 }
283
284 /**
285 * Creates a new Method exception instance.
286 *
287 * @param node the offending ASTnode
288 * @param name the method name
289 * @deprecated as of 3.2, use call with method arguments
290 */
291 @Deprecated
292 public Method(final JexlNode node, final String name) {
293 this(node, name, null);
294 }
295
296 /**
297 * Creates a new Method exception instance.
298 *
299 * @param node the offending ASTnode
300 * @param name the method name
301 * @param args the method arguments
302 * @since 3.2
303 */
304 public Method(final JexlNode node, final String name, final Object[] args) {
305 super(node, methodSignature(name, args));
306 }
307
308 @Override
309 protected String detailedMessage() {
310 return "unsolvable function/method '" + getMethodSignature() + "'";
311 }
312
313 /**
314 * Gets the method name
315 *
316 * @return the method name
317 */
318 public String getMethod() {
319 final String signature = getMethodSignature();
320 final int lparen = signature.indexOf('(');
321 return lparen > 0? signature.substring(0, lparen) : signature;
322 }
323
324 /**
325 * Gets the method signature.
326 *
327 * @return the method signature
328 * @since 3.2
329 */
330 public String getMethodSignature() {
331 return getDetail();
332 }
333 }
334
335 /**
336 * Thrown when an operator fails.
337 *
338 * @since 3.0
339 */
340 public static class Operator extends JexlException {
341 private static final long serialVersionUID = 20210606124100L;
342
343 /**
344 * Creates a new Operator exception instance.
345 *
346 * @param node the location information
347 * @param symbol the operator name
348 * @param cause the exception causing the error
349 */
350 public Operator(final JexlNode node, final String symbol, final Throwable cause) {
351 super(node, symbol, cause);
352 }
353
354 @Override
355 protected String detailedMessage() {
356 return "error calling operator '" + getSymbol() + "'";
357 }
358
359 /**
360 * Gets the method name
361 *
362 * @return the method name
363 */
364 public String getSymbol() {
365 return getDetail();
366 }
367 }
368
369 /**
370 * Thrown when parsing fails.
371 *
372 * @since 3.0
373 */
374 public static class Parsing extends JexlException {
375 private static final long serialVersionUID = 20210606123902L;
376
377 /**
378 * Creates a new Parsing exception instance.
379 *
380 * @param info the location information
381 * @param cause the javacc cause
382 */
383 public Parsing(final JexlInfo info, final ParseException cause) {
384 super(merge(info, cause), Objects.requireNonNull(cause).getAfter(), null);
385 }
386
387 /**
388 * Creates a new Parsing exception instance.
389 *
390 * @param info the location information
391 * @param msg the message
392 */
393 public Parsing(final JexlInfo info, final String msg) {
394 super(info, msg, null);
395 }
396
397 @Override
398 protected String detailedMessage() {
399 return parserError("parsing", getDetail());
400 }
401 }
402
403 /**
404 * Thrown when a property is unknown.
405 *
406 * @since 3.0
407 */
408 public static class Property extends JexlException {
409 private static final long serialVersionUID = 20210606123908L;
410
411 /**
412 * Undefined variable flag.
413 */
414 private final boolean undefined;
415
416 /**
417 * Creates a new Property exception instance.
418 *
419 * @param node the offending ASTnode
420 * @param pty the unknown property
421 * @deprecated 3.2
422 */
423 @Deprecated
424 public Property(final JexlNode node, final String pty) {
425 this(node, pty, true, null);
426 }
427
428 /**
429 * Creates a new Property exception instance.
430 *
431 * @param node the offending ASTnode
432 * @param pty the unknown property
433 * @param undef whether the variable is null or undefined
434 * @param cause the exception causing the error
435 */
436 public Property(final JexlNode node, final String pty, final boolean undef, final Throwable cause) {
437 super(node, pty, cause);
438 undefined = undef;
439 }
440
441 /**
442 * Creates a new Property exception instance.
443 *
444 * @param node the offending ASTnode
445 * @param pty the unknown property
446 * @param cause the exception causing the error
447 * @deprecated 3.2
448 */
449 @Deprecated
450 public Property(final JexlNode node, final String pty, final Throwable cause) {
451 this(node, pty, true, cause);
452 }
453
454 @Override
455 protected String detailedMessage() {
456 return (undefined? "undefined" : "null value") + " property '" + getProperty() + "'";
457 }
458
459 /**
460 * Gets the property name
461 *
462 * @return the property name
463 */
464 public String getProperty() {
465 return getDetail();
466 }
467
468 /**
469 * Tests whether the variable causing an error is undefined or evaluated as null.
470 *
471 * @return true if undefined, false otherwise
472 */
473 public boolean isUndefined() {
474 return undefined;
475 }
476 }
477
478 /**
479 * Thrown to return a value.
480 *
481 * @since 3.0
482 */
483 public static class Return extends JexlException {
484 private static final long serialVersionUID = 20210606124102L;
485
486 /** The returned value. */
487 private final transient Object result;
488
489 /**
490 * Creates a new instance of Return.
491 *
492 * @param node the return node
493 * @param msg the message
494 * @param value the returned value
495 */
496 public Return(final JexlNode node, final String msg, final Object value) {
497 super(node, msg, null, false);
498 this.result = value;
499 }
500
501 /**
502 * Gets the returned value
503 *
504 * @return the returned value
505 */
506 public Object getValue() {
507 return result;
508 }
509 }
510
511 /**
512 * Thrown when reaching stack-overflow.
513 *
514 * @since 3.2
515 */
516 public static class StackOverflow extends JexlException {
517 private static final long serialVersionUID = 20210606123904L;
518
519 /**
520 * Creates a new stack overflow exception instance.
521 *
522 * @param info the location information
523 * @param name the unknown method
524 * @param cause the exception causing the error
525 */
526 public StackOverflow(final JexlInfo info, final String name, final Throwable cause) {
527 super(info, name, cause);
528 }
529
530 @Override
531 protected String detailedMessage() {
532 return "stack overflow " + getDetail();
533 }
534 }
535
536 /**
537 * Thrown to throw a value.
538 *
539 * @since 3.3.1
540 */
541 public static class Throw extends JexlException {
542 private static final long serialVersionUID = 20210606124102L;
543
544 /** The thrown value. */
545 private final transient Object result;
546
547 /**
548 * Creates a new instance of Throw.
549 *
550 * @param node the throw node
551 * @param value the thrown value
552 */
553 public Throw(final JexlNode node, final Object value) {
554 super(node, null, null, false);
555 this.result = value;
556 }
557
558 /**
559 * Gets the thrown value
560 *
561 * @return the thrown value
562 */
563 public Object getValue() {
564 return result;
565 }
566 }
567
568 /**
569 * Thrown when tokenization fails.
570 *
571 * @since 3.0
572 */
573 public static class Tokenization extends JexlException {
574 private static final long serialVersionUID = 20210606123901L;
575
576 /**
577 * Creates a new Tokenization exception instance.
578 *
579 * @param info the location info
580 * @param cause the javacc cause
581 */
582 public Tokenization(final JexlInfo info, final TokenMgrException cause) {
583 super(merge(info, cause), Objects.requireNonNull(cause).getAfter(), null);
584 }
585
586 @Override
587 protected String detailedMessage() {
588 return parserError("tokenization", getDetail());
589 }
590 }
591
592 /**
593 * Thrown when method/ctor invocation fails.
594 * <p>These wrap InvocationTargetException as runtime exception
595 * allowing to go through without signature modifications.
596 *
597 * @since 3.2
598 */
599 public static class TryFailed extends JexlException {
600 private static final long serialVersionUID = 20210606124105L;
601
602 /**
603 * Creates a new instance.
604 *
605 * @param xany the original invocation target exception
606 */
607 TryFailed(final InvocationTargetException xany) {
608 super((JexlInfo) null, "tryFailed", xany.getCause());
609 }
610 }
611
612 /**
613 * Thrown when a variable is unknown.
614 *
615 * @since 3.0
616 */
617 public static class Variable extends JexlException {
618 private static final long serialVersionUID = 20210606123907L;
619
620 /**
621 * Undefined variable flag.
622 */
623 private final VariableIssue issue;
624
625 /**
626 * Creates a new Variable exception instance.
627 *
628 * @param node the offending ASTnode
629 * @param var the unknown variable
630 * @param undef whether the variable is undefined or evaluated as null
631 */
632 public Variable(final JexlNode node, final String var, final boolean undef) {
633 this(node, var, undef ? VariableIssue.UNDEFINED : VariableIssue.NULLVALUE);
634 }
635
636 /**
637 * Creates a new Variable exception instance.
638 *
639 * @param node the offending ASTnode
640 * @param var the unknown variable
641 * @param vi the variable issue
642 */
643 public Variable(final JexlNode node, final String var, final VariableIssue vi) {
644 super(node, var, null);
645 issue = vi;
646 }
647
648 @Override
649 protected String detailedMessage() {
650 return issue.message(getVariable());
651 }
652
653 /**
654 * Gets the variable name
655 *
656 * @return the variable name
657 */
658 public String getVariable() {
659 return getDetail();
660 }
661
662 /**
663 * Tests whether the variable causing an error is undefined or evaluated as null.
664 *
665 * @return true if undefined, false otherwise
666 */
667 public boolean isUndefined() {
668 return issue == VariableIssue.UNDEFINED;
669 }
670 }
671
672 /**
673 * 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 }