| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.apache.commons.jexl3; |
| |
| import org.apache.commons.jexl3.internal.Debugger; |
| import org.apache.commons.jexl3.parser.JavaccError; |
| import org.apache.commons.jexl3.parser.JexlNode; |
| import org.apache.commons.jexl3.parser.ParseException; |
| import org.apache.commons.jexl3.parser.TokenMgrError; |
| |
| import java.lang.reflect.InvocationTargetException; |
| import java.lang.reflect.UndeclaredThrowableException; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * Wraps any error that might occur during interpretation of a script or expression. |
| * |
| * @since 2.0 |
| */ |
| public class JexlException extends RuntimeException { |
| |
| /** The point of origin for this exception. */ |
| private final transient JexlNode mark; |
| |
| /** The debug info. */ |
| private final transient JexlInfo info; |
| |
| /** Maximum number of characters around exception location. */ |
| private static final int MAX_EXCHARLOC = 42; |
| |
| /** |
| * Creates a new JexlException. |
| * |
| * @param node the node causing the error |
| * @param msg the error message |
| */ |
| public JexlException(JexlNode node, String msg) { |
| this(node, msg, null); |
| } |
| |
| /** |
| * Creates a new JexlException. |
| * |
| * @param node the node causing the error |
| * @param msg the error message |
| * @param cause the exception causing the error |
| */ |
| public JexlException(JexlNode node, String msg, Throwable cause) { |
| super(msg != null ? msg : "", unwrap(cause)); |
| if (node != null) { |
| mark = node; |
| info = node.jexlInfo(); |
| } else { |
| mark = null; |
| info = null; |
| } |
| } |
| |
| /** |
| * Creates a new JexlException. |
| * |
| * @param jinfo the debugging information associated |
| * @param msg the error message |
| * @param cause the exception causing the error |
| */ |
| public JexlException(JexlInfo jinfo, String msg, Throwable cause) { |
| super(msg != null ? msg : "", unwrap(cause)); |
| mark = null; |
| info = jinfo; |
| } |
| |
| /** |
| * Gets the specific information for this exception. |
| * |
| * @return the information |
| */ |
| public JexlInfo getInfo() { |
| return getInfo(mark, info); |
| } |
| |
| /** |
| * Creates a string builder pre-filled with common error information (if possible). |
| * |
| * @param node the node |
| * @return a string builder |
| */ |
| private static StringBuilder errorAt(JexlNode node) { |
| JexlInfo info = node != null? getInfo(node, node.jexlInfo()) : null; |
| StringBuilder msg = new StringBuilder(); |
| if (info != null) { |
| msg.append(info.toString()); |
| } else { |
| msg.append("?:"); |
| } |
| msg.append(' '); |
| return msg; |
| } |
| |
| /** |
| * Gets the most specific information attached to a node. |
| * |
| * @param node the node |
| * @param info the information |
| * @return the information or null |
| */ |
| public static JexlInfo getInfo(JexlNode node, JexlInfo info) { |
| if (info != null && node != null) { |
| final Debugger dbg = new Debugger(); |
| if (dbg.debug(node)) { |
| return new JexlInfo(info) { |
| @Override |
| public JexlInfo.Detail getDetail() { |
| return dbg; |
| } |
| }; |
| } |
| } |
| return info; |
| } |
| |
| /** |
| * Cleans a JexlException from any org.apache.commons.jexl3.internal stack trace element. |
| * |
| * @return this exception |
| */ |
| public JexlException clean() { |
| return clean(this); |
| } |
| |
| /** |
| * Cleans a Throwable from any org.apache.commons.jexl3.internal stack trace element. |
| * |
| * @param <X> the throwable type |
| * @param xthrow the thowable |
| * @return the throwable |
| */ |
| private static <X extends Throwable> X clean(X xthrow) { |
| if (xthrow != null) { |
| List<StackTraceElement> stackJexl = new ArrayList<StackTraceElement>(); |
| for (StackTraceElement se : xthrow.getStackTrace()) { |
| String className = se.getClassName(); |
| if (!className.startsWith("org.apache.commons.jexl3.internal") |
| && !className.startsWith("org.apache.commons.jexl3.parser")) { |
| stackJexl.add(se); |
| } |
| } |
| xthrow.setStackTrace(stackJexl.toArray(new StackTraceElement[stackJexl.size()])); |
| } |
| return xthrow; |
| } |
| |
| /** |
| * Unwraps the cause of a throwable due to reflection. |
| * |
| * @param xthrow the throwable |
| * @return the cause |
| */ |
| private static Throwable unwrap(Throwable xthrow) { |
| if (xthrow instanceof InvocationTargetException) { |
| return xthrow.getCause(); |
| } else if (xthrow instanceof UndeclaredThrowableException) { |
| return xthrow.getCause(); |
| } else { |
| return xthrow; |
| } |
| } |
| |
| /** |
| * Merge the node info and the cause info to obtain best possible location. |
| * |
| * @param info the node |
| * @param cause the cause |
| * @return the info to use |
| */ |
| private static JexlInfo merge(JexlInfo info, JavaccError cause) { |
| JexlInfo dbgn = info != null ? info : null; |
| if (cause == null) { |
| return dbgn; |
| } else if (dbgn == null) { |
| return new JexlInfo("", cause.getLine(), cause.getColumn()); |
| } else { |
| return new JexlInfo(dbgn.getName(), cause.getLine(), cause.getColumn()); |
| } |
| } |
| |
| /** |
| * Accesses detailed message. |
| * |
| * @return the message |
| */ |
| protected String detailedMessage() { |
| return super.getMessage(); |
| } |
| |
| /** |
| * Formats an error message from the parser. |
| * |
| * @param prefix the prefix to the message |
| * @param expr the expression in error |
| * @return the formatted message |
| */ |
| protected String parserError(String prefix, String expr) { |
| int length = expr.length(); |
| if (length < MAX_EXCHARLOC) { |
| return prefix + " error in '" + expr + "'"; |
| } else { |
| int begin = info.getColumn(); |
| int end = begin + (MAX_EXCHARLOC / 2); |
| begin -= (MAX_EXCHARLOC / 2); |
| if (begin < 0) { |
| end -= begin; |
| begin = 0; |
| } |
| return prefix + " error near '... " |
| + expr.substring(begin, end > length ? length : end) + " ...'"; |
| } |
| } |
| |
| /** |
| * Thrown when tokenization fails. |
| * |
| * @since 3.0 |
| */ |
| public static class Tokenization extends JexlException { |
| /** |
| * Creates a new Tokenization exception instance. |
| * @param info the location info |
| * @param cause the javacc cause |
| */ |
| public Tokenization(JexlInfo info, TokenMgrError cause) { |
| super(merge(info, cause), cause.getAfter(), null); |
| } |
| |
| /** |
| * @return the specific detailed message |
| */ |
| public String getDetail() { |
| return super.detailedMessage(); |
| } |
| |
| @Override |
| protected String detailedMessage() { |
| return parserError("tokenization", getDetail()); |
| } |
| } |
| |
| /** |
| * Thrown when parsing fails. |
| * |
| * @since 3.0 |
| */ |
| public static class Parsing extends JexlException { |
| /** |
| * Creates a new Parsing exception instance. |
| * |
| * @param info the location information |
| * @param cause the javacc cause |
| */ |
| public Parsing(JexlInfo info, ParseException cause) { |
| super(merge(info, cause), cause.getAfter(), null); |
| } |
| |
| /** |
| * Creates a new Parsing exception instance. |
| * |
| * @param info the location information |
| * @param msg the message |
| */ |
| public Parsing(JexlInfo info, String msg) { |
| super(info, msg, null); |
| } |
| |
| /** |
| * @return the specific detailed message |
| */ |
| public String getDetail() { |
| return super.detailedMessage(); |
| } |
| |
| @Override |
| protected String detailedMessage() { |
| return parserError("parsing", getDetail()); |
| } |
| } |
| |
| /** |
| * Thrown when parsing fails due to an ambiguous statement. |
| * |
| * @since 3.0 |
| */ |
| public static class Ambiguous extends Parsing { |
| /** |
| * Creates a new Ambiguous statement exception instance. |
| * @param info the location information |
| * @param expr the source expression line |
| */ |
| public Ambiguous(JexlInfo info, String expr) { |
| super(info, expr); |
| } |
| |
| @Override |
| protected String detailedMessage() { |
| return parserError("ambiguous statement", getDetail()); |
| } |
| } |
| |
| /** |
| * Thrown when parsing fails due to an invalid assigment. |
| * |
| * @since 3.0 |
| */ |
| public static class Assignment extends Parsing { |
| /** |
| * Creates a new Assignment statement exception instance. |
| * |
| * @param info the location information |
| * @param expr the source expression line |
| */ |
| public Assignment(JexlInfo info, String expr) { |
| super(info, expr); |
| } |
| |
| @Override |
| protected String detailedMessage() { |
| return parserError("assignment", getDetail()); |
| } |
| } |
| |
| /** |
| * Thrown when a variable is unknown. |
| * |
| * @since 3.0 |
| */ |
| public static class Variable extends JexlException { |
| /** |
| * Undefined variable flag. |
| */ |
| private final boolean undefined; |
| /** |
| * Creates a new Variable exception instance. |
| * |
| * @param node the offending ASTnode |
| * @param var the unknown variable |
| * @param undef whether the variable is undefined or evaluated as null |
| */ |
| public Variable(JexlNode node, String var, boolean undef) { |
| super(node, var, null); |
| undefined = undef; |
| } |
| |
| /** |
| * Whether the variable causing an error is undefined or evaluated as null. |
| * |
| * @return true if undefined, false otherwise |
| */ |
| public boolean isUndefined() { |
| return undefined; |
| } |
| |
| /** |
| * @return the variable name |
| */ |
| public String getVariable() { |
| return super.detailedMessage(); |
| } |
| |
| @Override |
| protected String detailedMessage() { |
| return (undefined? "undefined" : "null value") + " variable " + getVariable(); |
| } |
| } |
| |
| /** |
| * Generates a message for a variable error. |
| * |
| * @param node the node where the error occurred |
| * @param variable the variable |
| * @param undef whether the variable is null or undefined |
| * @return the error message |
| */ |
| public static String variableError(JexlNode node, String variable, boolean undef) { |
| StringBuilder msg = errorAt(node); |
| if (undef) { |
| msg.append("undefined"); |
| } else { |
| msg.append("null value"); |
| } |
| msg.append(" variable "); |
| msg.append(variable); |
| return msg.toString(); |
| } |
| |
| /** |
| * Thrown when a property is unknown. |
| * |
| * @since 3.0 |
| */ |
| public static class Property extends JexlException { |
| /** |
| * Creates a new Property exception instance. |
| * |
| * @param node the offending ASTnode |
| * @param var the unknown variable |
| * @param cause the exception causing the error |
| */ |
| public Property(JexlNode node, String var, Throwable cause) { |
| super(node, var, cause); |
| } |
| |
| /** |
| * @return the property name |
| */ |
| public String getProperty() { |
| return super.detailedMessage(); |
| } |
| |
| @Override |
| protected String detailedMessage() { |
| return "unsolvable property '" + getProperty() + "'"; |
| } |
| } |
| |
| /** |
| * Generates a message for an unsolvable property error. |
| * |
| * @param node the node where the error occurred |
| * @param var the variable |
| * @return the error message |
| */ |
| public static String propertyError(JexlNode node, String var) { |
| StringBuilder msg = errorAt(node); |
| msg.append("unsolvable property '"); |
| msg.append(var); |
| msg.append('\''); |
| return msg.toString(); |
| } |
| |
| /** |
| * Thrown when a method or ctor is unknown, ambiguous or inaccessible. |
| * |
| * @since 3.0 |
| */ |
| public static class Method extends JexlException { |
| /** |
| * Creates a new Method exception instance. |
| * |
| * @param node the offending ASTnode |
| * @param name the method name |
| */ |
| public Method(JexlNode node, String name) { |
| super(node, name); |
| } |
| |
| /** |
| * Creates a new Method exception instance. |
| * |
| * @param info the location information |
| * @param name the unknown method |
| * @param cause the exception causing the error |
| */ |
| public Method(JexlInfo info, String name, Throwable cause) { |
| super(info, name, cause); |
| } |
| |
| /** |
| * @return the method name |
| */ |
| public String getMethod() { |
| return super.detailedMessage(); |
| } |
| |
| @Override |
| protected String detailedMessage() { |
| return "unsolvable function/method '" + getMethod() + "'"; |
| } |
| } |
| |
| /** |
| * Generates a message for a unsolvable method error. |
| * |
| * @param node the node where the error occurred |
| * @param method the method name |
| * @return the error message |
| */ |
| public static String methodError(JexlNode node, String method) { |
| StringBuilder msg = errorAt(node); |
| msg.append("unsolvable function/method '"); |
| msg.append(method); |
| msg.append('\''); |
| return msg.toString(); |
| } |
| |
| /** |
| * Thrown when an operator fails. |
| * |
| * @since 3.0 |
| */ |
| public static class Operator extends JexlException { |
| /** |
| * Creates a new Operator exception instance. |
| * |
| * @param node the location information |
| * @param symbol the operator name |
| * @param cause the exception causing the error |
| */ |
| public Operator(JexlNode node, String symbol, Throwable cause) { |
| super(node, symbol, cause); |
| } |
| |
| /** |
| * @return the method name |
| */ |
| public String getSymbol() { |
| return super.detailedMessage(); |
| } |
| |
| @Override |
| protected String detailedMessage() { |
| return "error calling operator '" + getSymbol() + "'"; |
| } |
| } |
| |
| /** |
| * Generates a message for an operator error. |
| * |
| * @param node the node where the error occurred |
| * @param symbol the operator name |
| * @return the error message |
| */ |
| public static String operatorError(JexlNode node, String symbol) { |
| StringBuilder msg = errorAt(node); |
| msg.append("error calling operator '"); |
| msg.append(symbol); |
| msg.append('\''); |
| return msg.toString(); |
| } |
| |
| /** |
| * Thrown when an annotation handler throws an exception. |
| * |
| * @since 3.1 |
| */ |
| public static class Annotation extends JexlException { |
| /** |
| * Creates a new Annotation exception instance. |
| * |
| * @param node the annotated statement node |
| * @param name the annotation name |
| * @param cause the exception causing the error |
| */ |
| public Annotation(JexlNode node, String name, Throwable cause) { |
| super(node, name, cause); |
| } |
| |
| /** |
| * @return the annotation name |
| */ |
| public String getAnnotation() { |
| return super.detailedMessage(); |
| } |
| |
| @Override |
| protected String detailedMessage() { |
| return "error processing annotation '" + getAnnotation() + "'"; |
| } |
| } |
| |
| /** |
| * Generates a message for an annotation error. |
| * |
| * @param node the node where the error occurred |
| * @param annotation the annotation name |
| * @return the error message |
| */ |
| public static String annotationError(JexlNode node, String annotation) { |
| StringBuilder msg = errorAt(node); |
| msg.append("error processing annotation '"); |
| msg.append(annotation); |
| msg.append('\''); |
| return msg.toString(); |
| } |
| |
| /** |
| * Thrown to return a value. |
| * |
| * @since 3.0 |
| */ |
| public static class Return extends JexlException { |
| |
| /** The returned value. */ |
| private final Object result; |
| |
| /** |
| * Creates a new instance of Return. |
| * |
| * @param node the return node |
| * @param msg the message |
| * @param value the returned value |
| */ |
| public Return(JexlNode node, String msg, Object value) { |
| super(node, msg, null); |
| this.result = value; |
| } |
| |
| /** |
| * @return the returned value |
| */ |
| public Object getValue() { |
| return result; |
| } |
| } |
| |
| /** |
| * Thrown to cancel a script execution. |
| * |
| * @since 3.0 |
| */ |
| public static class Cancel extends JexlException { |
| /** |
| * Creates a new instance of Cancel. |
| * |
| * @param node the node where the interruption was detected |
| */ |
| public Cancel(JexlNode node) { |
| super(node, "execution cancelled", null); |
| } |
| } |
| |
| /** |
| * Thrown to break a loop. |
| * |
| * @since 3.0 |
| */ |
| public static class Break extends JexlException { |
| /** |
| * Creates a new instance of Break. |
| * |
| * @param node the break |
| */ |
| public Break(JexlNode node) { |
| super(node, "break loop", null); |
| } |
| } |
| |
| /** |
| * Thrown to continue a loop. |
| * |
| * @since 3.0 |
| */ |
| public static class Continue extends JexlException { |
| /** |
| * Creates a new instance of Continue. |
| * |
| * @param node the continue |
| */ |
| public Continue(JexlNode node) { |
| super(node, "continue loop", null); |
| } |
| } |
| |
| /** |
| * Detailed info message about this error. |
| * Format is "debug![begin,end]: string \n msg" where: |
| * <ul></ul> |
| * - debug is the debugging information if it exists (@link JexlEngine.setDebug) |
| * - begin, end are character offsets in the string for the precise location of the error |
| * - string is the string representation of the offending expression |
| * - msg is the actual explanation message for this error |
| * |
| * @return this error as a string |
| */ |
| @Override |
| public String getMessage() { |
| StringBuilder msg = new StringBuilder(); |
| if (info != null) { |
| msg.append(info.toString()); |
| } else { |
| msg.append("?:"); |
| } |
| msg.append(' '); |
| msg.append(detailedMessage()); |
| Throwable cause = getCause(); |
| if (cause instanceof JexlArithmetic.NullOperand) { |
| msg.append(" caused by null operand"); |
| } |
| return msg.toString(); |
| } |
| } |