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 }