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 package org.apache.commons.jexl2;
018
019 import java.lang.reflect.InvocationTargetException;
020 import java.lang.reflect.UndeclaredThrowableException;
021 import org.apache.commons.jexl2.parser.JexlNode;
022 import org.apache.commons.jexl2.parser.ParseException;
023 import org.apache.commons.jexl2.parser.TokenMgrError;
024
025 /**
026 * Wraps any error that might occur during interpretation of a script or expression.
027 * @since 2.0
028 */
029 public class JexlException extends RuntimeException {
030 /** The point of origin for this exception. */
031 protected final transient JexlNode mark;
032 /** The debug info. */
033 protected final transient JexlInfo info;
034 /** A marker to use in NPEs stating a null operand error. */
035 public static final String NULL_OPERAND = "jexl.null";
036 /** Minimum number of characters around exception location. */
037 private static final int MIN_EXCHARLOC = 5;
038 /** Maximum number of characters around exception location. */
039 private static final int MAX_EXCHARLOC = 10;
040
041 /**
042 * Creates a new JexlException.
043 * @param node the node causing the error
044 * @param msg the error message
045 */
046 public JexlException(JexlNode node, String msg) {
047 super(msg);
048 mark = node;
049 info = node != null ? node.debugInfo() : null;
050
051 }
052
053 /**
054 * Creates a new JexlException.
055 * @param node the node causing the error
056 * @param msg the error message
057 * @param cause the exception causing the error
058 */
059 public JexlException(JexlNode node, String msg, Throwable cause) {
060 super(msg, unwrap(cause));
061 mark = node;
062 info = node != null ? node.debugInfo() : null;
063 }
064
065 /**
066 * Creates a new JexlException.
067 * @param dbg the debugging information associated
068 * @param msg the error message
069 */
070 public JexlException(JexlInfo dbg, String msg) {
071 super(msg);
072 mark = null;
073 info = dbg;
074 }
075
076 /**
077 * Creates a new JexlException.
078 * @param dbg the debugging information associated
079 * @param msg the error message
080 * @param cause the exception causing the error
081 */
082 public JexlException(JexlInfo dbg, String msg, Throwable cause) {
083 super(msg, unwrap(cause));
084 mark = null;
085 info = dbg;
086 }
087
088 /**
089 * Unwraps the cause of a throwable due to reflection.
090 * @param xthrow the throwable
091 * @return the cause
092 */
093 private static Throwable unwrap(Throwable xthrow) {
094 if (xthrow instanceof InvocationTargetException) {
095 return ((InvocationTargetException) xthrow).getTargetException();
096 } else if (xthrow instanceof UndeclaredThrowableException) {
097 return ((UndeclaredThrowableException) xthrow).getUndeclaredThrowable();
098 } else {
099 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 }