View Javadoc

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    *      http://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  package org.apache.commons.jexl2;
18  
19  import java.lang.reflect.InvocationTargetException;
20  import java.lang.reflect.UndeclaredThrowableException;
21  import org.apache.commons.jexl2.parser.JexlNode;
22  import org.apache.commons.jexl2.parser.ParseException;
23  import org.apache.commons.jexl2.parser.TokenMgrError;
24  
25  /**
26   * Wraps any error that might occur during interpretation of a script or expression.
27   * @since 2.0
28   */
29  public class JexlException extends RuntimeException {
30      /** The point of origin for this exception. */
31      protected final transient JexlNode mark;
32      /** The debug info. */
33      protected final transient JexlInfo info;
34      /** A marker to use in NPEs stating a null operand error. */
35      public static final String NULL_OPERAND = "jexl.null";
36      /** Minimum number of characters around exception location. */
37      private static final int MIN_EXCHARLOC = 5;
38      /** Maximum number of characters around exception location. */
39      private static final int MAX_EXCHARLOC = 10;
40  
41      /**
42       * Creates a new JexlException.
43       * @param node the node causing the error
44       * @param msg the error message
45       */
46      public JexlException(JexlNode node, String msg) {
47          super(msg);
48          mark = node;
49          info = node != null ? node.debugInfo() : null;
50  
51      }
52  
53      /**
54       * Creates a new JexlException.
55       * @param node the node causing the error
56       * @param msg the error message
57       * @param cause the exception causing the error
58       */
59      public JexlException(JexlNode node, String msg, Throwable cause) {
60          super(msg, unwrap(cause));
61          mark = node;
62          info = node != null ? node.debugInfo() : null;
63      }
64  
65      /**
66       * Creates a new JexlException.
67       * @param dbg the debugging information associated
68       * @param msg the error message
69       */
70      public JexlException(JexlInfo dbg, String msg) {
71          super(msg);
72          mark = null;
73          info = dbg;
74      }
75  
76      /**
77       * Creates a new JexlException.
78       * @param dbg the debugging information associated
79       * @param msg the error message
80       * @param cause the exception causing the error
81       */
82      public JexlException(JexlInfo dbg, String msg, Throwable cause) {
83          super(msg, unwrap(cause));
84          mark = null;
85          info = dbg;
86      }
87  
88      /**
89       * Unwraps the cause of a throwable due to reflection. 
90       * @param xthrow the throwable
91       * @return the cause
92       */
93      private static Throwable unwrap(Throwable xthrow) {
94          if (xthrow instanceof InvocationTargetException) {
95              return ((InvocationTargetException) xthrow).getTargetException();
96          } else if (xthrow instanceof UndeclaredThrowableException) {
97              return ((UndeclaredThrowableException) xthrow).getUndeclaredThrowable();
98          } else {
99              return xthrow;
100         }
101     }
102 
103     /**
104      * Accesses detailed message.
105      * @return  the message
106      * @since 2.1
107      */
108     protected String detailedMessage() {
109         return super.getMessage();
110     }
111 
112     /**
113      * Formats an error message from the parser.
114      * @param prefix the prefix to the message
115      * @param expr the expression in error
116      * @return the formatted message
117      * @since 2.1
118      */
119     protected String parserError(String prefix, String expr) {
120         int begin = info.debugInfo().getColumn();
121         int end = begin + MIN_EXCHARLOC;
122         begin -= MIN_EXCHARLOC;
123         if (begin < 0) {
124             end += MIN_EXCHARLOC;
125             begin = 0;
126         }
127         int length = expr.length();
128         if (length < MAX_EXCHARLOC) {
129             return prefix + " error in '" + expr + "'";
130         } else {
131             return prefix + " error near '... "
132                     + expr.substring(begin, end > length ? length : end) + " ...'";
133         }
134     }
135 
136     /**
137      * Thrown when tokenization fails.
138      * @since 2.1
139      */
140     public static class Tokenization extends JexlException {
141         /**
142          * Creates a new Tokenization exception instance.
143          * @param node the location info
144          * @param expr the expression
145          * @param cause the javacc cause
146          */
147         public Tokenization(JexlInfo node, CharSequence expr, TokenMgrError cause) {
148             super(merge(node, cause), expr.toString(), cause);
149         }
150 
151         /**
152          * Merge the node info and the cause info to obtain best possible location.
153          * @param node the node
154          * @param cause the cause
155          * @return the info to use
156          */
157         private static DebugInfo merge(JexlInfo node, TokenMgrError cause) {
158             DebugInfo dbgn = node != null ? node.debugInfo() : null;
159             if (cause == null) {
160                 return dbgn;
161             } else if (dbgn == null) {
162                 return new DebugInfo("", cause.getLine(), cause.getColumn());
163             } else {
164                 return new DebugInfo(dbgn.getName(), cause.getLine(), cause.getColumn());
165             }
166         }
167 
168         /**
169          * @return the expression
170          */
171         public String getExpression() {
172             return super.detailedMessage();
173         }
174 
175         @Override
176         protected String detailedMessage() {
177             return parserError("tokenization", getExpression());
178         }
179     }
180 
181     /**
182      * Thrown when parsing fails.
183      * @since 2.1
184      */
185     public static class Parsing extends JexlException {
186         /**
187          * Creates a new Variable exception instance.
188          * @param node the offending ASTnode
189          * @param expr the offending source
190          * @param cause the javacc cause
191          */
192         public Parsing(JexlInfo node, CharSequence expr, ParseException cause) {
193             super(merge(node, cause), expr.toString(), cause);
194         }
195 
196         /**
197          * Merge the node info and the cause info to obtain best possible location.
198          * @param node the node
199          * @param cause the cause
200          * @return the info to use
201          */
202         private static DebugInfo merge(JexlInfo node, ParseException cause) {
203             DebugInfo dbgn = node != null ? node.debugInfo() : null;
204             if (cause == null) {
205                 return dbgn;
206             } else if (dbgn == null) {
207                 return new DebugInfo("", cause.getLine(), cause.getColumn());
208             } else {
209                 return new DebugInfo(dbgn.getName(), cause.getLine(), cause.getColumn());
210             }
211         }
212 
213         /**
214          * @return the expression
215          */
216         public String getExpression() {
217             return super.detailedMessage();
218         }
219 
220         @Override
221         protected String detailedMessage() {
222             return parserError("parsing", getExpression());
223         }
224     }
225 
226     /**
227      * Thrown when a variable is unknown.
228      * @since 2.1
229      */
230     public static class Variable extends JexlException {
231         /**
232          * Creates a new Variable exception instance.
233          * @param node the offending ASTnode
234          * @param var the unknown variable
235          */
236         public Variable(JexlNode node, String var) {
237             super(node, var);
238         }
239 
240         /**
241          * @return the variable name
242          */
243         public String getVariable() {
244             return super.detailedMessage();
245         }
246 
247         @Override
248         protected String detailedMessage() {
249             return "undefined variable " + getVariable();
250         }
251     }
252 
253     /**
254      * Thrown when a property is unknown.
255      * @since 2.1
256      */
257     public static class Property extends JexlException {
258         /**
259          * Creates a new Property exception instance.
260          * @param node the offending ASTnode
261          * @param var the unknown variable
262          */
263         public Property(JexlNode node, String var) {
264             super(node, var);
265         }
266 
267         /**
268          * @return the property name
269          */
270         public String getProperty() {
271             return super.detailedMessage();
272         }
273 
274         @Override
275         protected String detailedMessage() {
276             return "inaccessible or unknown property " + getProperty();
277         }
278     }
279 
280     /**
281      * Thrown when a method or ctor is unknown, ambiguous or inaccessible.
282      * @since 2.1
283      */
284     public static class Method extends JexlException {
285         /**
286          * Creates a new Method exception instance.
287          * @param node the offending ASTnode
288          * @param name the unknown method
289          */
290         public Method(JexlNode node, String name) {
291             super(node, name);
292         }
293 
294         /**
295          * @return the method name
296          */
297         public String getMethod() {
298             return super.detailedMessage();
299         }
300 
301         @Override
302         protected String detailedMessage() {
303             return "unknown, ambiguous or inaccessible method " + getMethod();
304         }
305     }
306 
307     /**
308      * Thrown to return a value.
309      * @since 2.1
310      */
311     protected static class Return extends JexlException {
312         /** The returned value. */
313         private final Object result;
314 
315         /**
316          * Creates a new instance of Return.
317          * @param node the return node
318          * @param msg the message
319          * @param value the returned value
320          */
321         protected Return(JexlNode node, String msg, Object value) {
322             super(node, msg);
323             this.result = value;
324         }
325 
326         /**
327          * @return the returned value
328          */
329         public Object getValue() {
330             return result;
331         }
332     }
333 
334     /**
335      * Thrown to cancel a script execution.
336      * @since 2.1
337      */
338     protected static class Cancel extends JexlException {
339         /**
340          * Creates a new instance of Cancel.
341          * @param node the node where the interruption was detected
342          */
343         protected Cancel(JexlNode node) {
344             super(node, "execution cancelled", null);
345         }
346     }
347 
348     /**
349      * Gets information about the cause of this error.
350      * <p>
351      * The returned string represents the outermost expression in error.
352      * The info parameter, an int[2] optionally provided by the caller, will be filled with the begin/end offset
353      * characters of the precise error's trigger.
354      * </p>
355      * @param offsets character offset interval of the precise node triggering the error
356      * @return a string representation of the offending expression, the empty string if it could not be determined
357      */
358     public String getInfo(int[] offsets) {
359         Debugger dbg = new Debugger();
360         if (dbg.debug(mark)) {
361             if (offsets != null && offsets.length >= 2) {
362                 offsets[0] = dbg.start();
363                 offsets[1] = dbg.end();
364             }
365             return dbg.data();
366         }
367         return "";
368     }
369 
370     /**
371      * Detailed info message about this error.
372      * Format is "debug![begin,end]: string \n msg" where:
373      * - debug is the debugging information if it exists (@link JexlEngine.setDebug)
374      * - begin, end are character offsets in the string for the precise location of the error
375      * - string is the string representation of the offending expression
376      * - msg is the actual explanation message for this error
377      * @return this error as a string
378      */
379     @Override
380     public String getMessage() {
381         Debugger dbg = new Debugger();
382         StringBuilder msg = new StringBuilder();
383         if (info != null) {
384             msg.append(info.debugString());
385         }
386         if (dbg.debug(mark)) {
387             msg.append("![");
388             msg.append(dbg.start());
389             msg.append(",");
390             msg.append(dbg.end());
391             msg.append("]: '");
392             msg.append(dbg.data());
393             msg.append("'");
394         }
395         msg.append(' ');
396         msg.append(detailedMessage());
397         Throwable cause = getCause();
398         if (cause != null && NULL_OPERAND == cause.getMessage()) {
399             msg.append(" caused by null operand");
400         }
401         return msg.toString();
402     }
403 }